add summarizing discovery controller and handlers

This commit is contained in:
deads2k 2016-12-07 10:55:33 -05:00
parent 79f497bca7
commit fb9c109953
17 changed files with 1042 additions and 5 deletions

View File

@ -0,0 +1,11 @@
apiVersion: apiregistration.k8s.io/v1alpha1
kind: APIService
metadata:
name: v1.
spec:
version: v1
service:
namespace: default
name: kubernetes
insecureSkipTLSVerify: true
priority: 100

View File

@ -0,0 +1,12 @@
apiVersion: apiregistration.k8s.io/v1alpha1
kind: APIService
metadata:
name: v1.autoscaling
spec:
group: autoscaling
version: v1
service:
namespace: default
name: kubernetes
insecureSkipTLSVerify: true
priority: 100

View File

@ -0,0 +1,12 @@
apiVersion: apiregistration.k8s.io/v1alpha1
kind: APIService
metadata:
name: v1.batch
spec:
group: batch
version: v1
service:
namespace: default
name: kubernetes
insecureSkipTLSVerify: true
priority: 100

View File

@ -0,0 +1,12 @@
apiVersion: apiregistration.k8s.io/v1alpha1
kind: APIService
metadata:
name: v1alpha1.certificates.k8s.io
spec:
group: certificates.k8s.io
version: v1alpha1
service:
namespace: default
name: kubernetes
insecureSkipTLSVerify: true
priority: 100

View File

@ -0,0 +1,12 @@
apiVersion: apiregistration.k8s.io/v1alpha1
kind: APIService
metadata:
name: v1alpha1.rbac.authorization.k8s.io
spec:
group: rbac.authorization.k8s.io
version: v1alpha1
service:
namespace: default
name: kubernetes
insecureSkipTLSVerify: true
priority: 100

View File

@ -0,0 +1,12 @@
apiVersion: apiregistration.k8s.io/v1alpha1
kind: APIService
metadata:
name: v1beta1.apps
spec:
group: apps
version: v1beta1
service:
namespace: default
name: kubernetes
insecureSkipTLSVerify: true
priority: 100

View File

@ -0,0 +1,12 @@
apiVersion: apiregistration.k8s.io/v1alpha1
kind: APIService
metadata:
name: v1beta1.authentication.k8s.io
spec:
group: authentication.k8s.io
version: v1beta1
service:
namespace: default
name: kubernetes
insecureSkipTLSVerify: true
priority: 100

View File

@ -0,0 +1,12 @@
apiVersion: apiregistration.k8s.io/v1alpha1
kind: APIService
metadata:
name: v1beta1.authorization.k8s.io
spec:
group: authorization.k8s.io
version: v1beta1
service:
namespace: default
name: kubernetes
insecureSkipTLSVerify: true
priority: 100

View File

@ -0,0 +1,12 @@
apiVersion: apiregistration.k8s.io/v1alpha1
kind: APIService
metadata:
name: v1beta1.extensions
spec:
group: extensions
version: v1beta1
service:
namespace: default
name: kubernetes
insecureSkipTLSVerify: true
priority: 150

View File

@ -0,0 +1,12 @@
apiVersion: apiregistration.k8s.io/v1alpha1
kind: APIService
metadata:
name: v1beta1.policy
spec:
group: policy
version: v1beta1
service:
namespace: default
name: kubernetes
insecureSkipTLSVerify: true
priority: 100

View File

@ -0,0 +1,12 @@
apiVersion: apiregistration.k8s.io/v1alpha1
kind: APIService
metadata:
name: v1beta1.storage.k8s.io
spec:
group: storage.k8s.io
version: v1beta1
service:
namespace: default
name: kubernetes
insecureSkipTLSVerify: true
priority: 100

View File

@ -12,16 +12,57 @@ load(
go_library(
name = "go_default_library",
srcs = ["apiserver.go"],
srcs = [
"apiserver.go",
"apiservice_controller.go",
"handler_apis.go",
],
tags = ["automanaged"],
deps = [
"//cmd/kubernetes-discovery/pkg/apis/apiregistration:go_default_library",
"//cmd/kubernetes-discovery/pkg/apis/apiregistration/v1alpha1:go_default_library",
"//cmd/kubernetes-discovery/pkg/client/clientset_generated/internalclientset:go_default_library",
"//cmd/kubernetes-discovery/pkg/client/clientset_generated/release_1_5:go_default_library",
"//cmd/kubernetes-discovery/pkg/client/informers:go_default_library",
"//cmd/kubernetes-discovery/pkg/client/informers/apiregistration/internalversion:go_default_library",
"//cmd/kubernetes-discovery/pkg/client/listers/apiregistration/internalversion:go_default_library",
"//cmd/kubernetes-discovery/pkg/registry/apiservice:go_default_library",
"//pkg/api:go_default_library",
"//pkg/api/errors:go_default_library",
"//pkg/api/rest:go_default_library",
"//pkg/apis/meta/v1:go_default_library",
"//pkg/apiserver:go_default_library",
"//pkg/apiserver/filters:go_default_library",
"//pkg/auth/handlers:go_default_library",
"//pkg/client/cache:go_default_library",
"//pkg/controller:go_default_library",
"//pkg/genericapiserver:go_default_library",
"//pkg/genericapiserver/filters:go_default_library",
"//pkg/labels:go_default_library",
"//pkg/registry/generic:go_default_library",
"//pkg/runtime:go_default_library",
"//pkg/runtime/schema:go_default_library",
"//pkg/util/runtime:go_default_library",
"//pkg/util/sets:go_default_library",
"//pkg/util/wait:go_default_library",
"//pkg/util/workqueue:go_default_library",
"//pkg/version:go_default_library",
"//vendor:github.com/golang/glog",
],
)
go_test(
name = "go_default_test",
srcs = ["handler_apis_test.go"],
library = "go_default_library",
tags = ["automanaged"],
deps = [
"//cmd/kubernetes-discovery/pkg/apis/apiregistration:go_default_library",
"//cmd/kubernetes-discovery/pkg/client/listers/apiregistration/internalversion:go_default_library",
"//pkg/api:go_default_library",
"//pkg/apis/meta/v1:go_default_library",
"//pkg/client/cache:go_default_library",
"//pkg/runtime:go_default_library",
"//pkg/util/diff:go_default_library",
],
)

View File

@ -17,17 +17,34 @@ limitations under the License.
package apiserver
import (
"net/http"
"os"
"time"
"k8s.io/kubernetes/pkg/api"
"k8s.io/kubernetes/pkg/api/rest"
apiserverfilters "k8s.io/kubernetes/pkg/apiserver/filters"
authhandlers "k8s.io/kubernetes/pkg/auth/handlers"
"k8s.io/kubernetes/pkg/genericapiserver"
genericfilters "k8s.io/kubernetes/pkg/genericapiserver/filters"
"k8s.io/kubernetes/pkg/registry/generic"
"k8s.io/kubernetes/pkg/runtime/schema"
"k8s.io/kubernetes/pkg/util/sets"
"k8s.io/kubernetes/pkg/util/wait"
"k8s.io/kubernetes/pkg/version"
"k8s.io/kubernetes/cmd/kubernetes-discovery/pkg/apis/apiregistration"
"k8s.io/kubernetes/cmd/kubernetes-discovery/pkg/apis/apiregistration/v1alpha1"
"k8s.io/kubernetes/cmd/kubernetes-discovery/pkg/client/clientset_generated/internalclientset"
clientset "k8s.io/kubernetes/cmd/kubernetes-discovery/pkg/client/clientset_generated/release_1_5"
"k8s.io/kubernetes/cmd/kubernetes-discovery/pkg/client/informers"
listers "k8s.io/kubernetes/cmd/kubernetes-discovery/pkg/client/listers/apiregistration/internalversion"
apiservicestorage "k8s.io/kubernetes/cmd/kubernetes-discovery/pkg/registry/apiservice"
)
// legacyAPIServiceName is the fixed name of the only non-groupified API version
const legacyAPIServiceName = "v1."
// TODO move to genericapiserver or something like that
// RESTOptionsGetter is used to construct storage for a particular resource
type RESTOptionsGetter interface {
@ -44,6 +61,14 @@ type Config struct {
// APIDiscoveryServer contains state for a Kubernetes cluster master/api server.
type APIDiscoveryServer struct {
GenericAPIServer *genericapiserver.GenericAPIServer
// handledAPIServices tracks which APIServices have already been handled. Once endpoints are added,
// the listers that are used keep bits in sync automatically.
handledAPIServices sets.String
// lister is used to add group handling for /apis/<group> discovery lookups based on
// controller state
lister listers.APIServiceLister
}
type completedConfig struct {
@ -67,26 +92,108 @@ func (c *Config) SkipComplete() completedConfig {
// New returns a new instance of APIDiscoveryServer from the given config.
func (c completedConfig) New() (*APIDiscoveryServer, error) {
informerFactory := informers.NewSharedInformerFactory(
internalclientset.NewForConfigOrDie(c.Config.GenericConfig.LoopbackClientConfig),
clientset.NewForConfigOrDie(c.Config.GenericConfig.LoopbackClientConfig),
5*time.Minute, // this is effectively used as a refresh interval right now. Might want to do something nicer later on.
)
// 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{
informers: informerFactory,
}).handlerChain
genericServer, err := c.Config.GenericConfig.SkipComplete().New() // completion is done in Complete, no need for a second time
if err != nil {
return nil, err
}
s := &APIDiscoveryServer{
GenericAPIServer: genericServer,
GenericAPIServer: genericServer,
handledAPIServices: sets.String{},
lister: informerFactory.Apiregistration().InternalVersion().APIServices().Lister(),
}
apiGroupInfo := genericapiserver.NewDefaultAPIGroupInfo(apiregistration.GroupName)
apiGroupInfo.GroupMeta.GroupVersion = v1alpha1.SchemeGroupVersion
v1alpha1storage := map[string]rest.Storage{}
v1alpha1storage["apiservices"] = apiservicestorage.NewREST(c.RESTOptionsGetter.NewFor(apiregistration.Resource("apiservices")))
apiGroupInfo.VersionedResourcesStorageMap["v1alpha1"] = v1alpha1storage
if err := s.GenericAPIServer.InstallAPIGroup(&apiGroupInfo); err != nil {
return nil, err
}
apiserviceRegistrationController := NewAPIServiceRegistrationController(informerFactory.Apiregistration().InternalVersion().APIServices(), s)
s.GenericAPIServer.AddPostStartHook("start-informers", func(context genericapiserver.PostStartHookContext) error {
informerFactory.Start(wait.NeverStop)
return nil
})
s.GenericAPIServer.AddPostStartHook("apiservice-registration-controller", func(context genericapiserver.PostStartHookContext) error {
apiserviceRegistrationController.Run(wait.NeverStop)
return nil
})
return s, nil
}
// handlerChainConfig is the config used to build the custom handler chain for this api server
type handlerChainConfig struct {
informers informers.SharedInformerFactory
}
// handlerChain is a method to build the handler chain for this API server. We need a custom handler chain so that we
// can have custom handling for `/apis`, since we're hosting discovery differently from anyone else and we're hosting
// the endpoints differently, since we're proxying all groups except for apiregistration.k8s.io.
func (h *handlerChainConfig) handlerChain(apiHandler http.Handler, c *genericapiserver.Config) (secure, insecure http.Handler) {
// add this as a filter so that we never collide with "already registered" failures on `/apis`
handler := WithAPIs(apiHandler, h.informers.Apiregistration().InternalVersion().APIServices())
handler = apiserverfilters.WithAuthorization(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
handler = apiserverfilters.WithAudit(handler, c.RequestContextMapper, os.Stdout)
handler = authhandlers.WithAuthentication(handler, c.RequestContextMapper, c.Authenticator, authhandlers.Unauthorized(c.SupportsBasicAuth))
handler = genericfilters.WithCORS(handler, c.CorsAllowedOriginList, nil, nil, nil, "true")
handler = genericfilters.WithPanicRecovery(handler, c.RequestContextMapper)
handler = genericfilters.WithTimeoutForNonLongRunningRequests(handler, c.RequestContextMapper, c.LongRunningFunc)
handler = genericfilters.WithMaxInFlightLimit(handler, c.MaxRequestsInFlight, c.MaxMutatingRequestsInFlight, c.RequestContextMapper, c.LongRunningFunc)
handler = apiserverfilters.WithRequestInfo(handler, genericapiserver.NewRequestInfoResolver(c), c.RequestContextMapper)
handler = api.WithRequestContext(handler, c.RequestContextMapper)
return handler, nil
}
// 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
func (s *APIDiscoveryServer) AddAPIService(apiService *apiregistration.APIService) {
if s.handledAPIServices.Has(apiService.Name) {
return
}
// if we're dealing with the legacy group, we're done here
if apiService.Name == legacyAPIServiceName {
s.handledAPIServices.Insert(apiService.Name)
return
}
// it's time to register the group discovery endpoint
groupPath := "/apis/" + apiService.Spec.Group
groupDiscoveryHandler := &apiGroupHandler{
groupName: apiService.Spec.Group,
lister: s.lister,
}
// discovery is protected
s.GenericAPIServer.HandlerContainer.SecretRoutes.Handle(groupPath, groupDiscoveryHandler)
s.GenericAPIServer.HandlerContainer.SecretRoutes.Handle(groupPath+"/", groupDiscoveryHandler)
}
// 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
func (s *APIDiscoveryServer) RemoveAPIService(apiServiceName string) {
if !s.handledAPIServices.Has(apiServiceName) {
return
}
}

View File

@ -0,0 +1,162 @@
/*
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 (
"fmt"
"time"
"github.com/golang/glog"
apierrors "k8s.io/kubernetes/pkg/api/errors"
"k8s.io/kubernetes/pkg/client/cache"
"k8s.io/kubernetes/pkg/controller"
utilruntime "k8s.io/kubernetes/pkg/util/runtime"
"k8s.io/kubernetes/pkg/util/wait"
"k8s.io/kubernetes/pkg/util/workqueue"
"k8s.io/kubernetes/cmd/kubernetes-discovery/pkg/apis/apiregistration"
informers "k8s.io/kubernetes/cmd/kubernetes-discovery/pkg/client/informers/apiregistration/internalversion"
listers "k8s.io/kubernetes/cmd/kubernetes-discovery/pkg/client/listers/apiregistration/internalversion"
)
type APIHandlerManager interface {
AddAPIService(apiServer *apiregistration.APIService)
RemoveAPIService(apiServerName string)
}
type APIServiceRegistrationController struct {
apiHandlerManager APIHandlerManager
apiServerLister listers.APIServiceLister
// To allow injection for testing.
syncFn func(key string) error
queue workqueue.RateLimitingInterface
}
func NewAPIServiceRegistrationController(apiServerInformer informers.APIServiceInformer, apiHandlerManager APIHandlerManager) *APIServiceRegistrationController {
c := &APIServiceRegistrationController{
apiHandlerManager: apiHandlerManager,
apiServerLister: apiServerInformer.Lister(),
queue: workqueue.NewNamedRateLimitingQueue(workqueue.DefaultControllerRateLimiter(), "APIServiceRegistrationController"),
}
apiServerInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{
AddFunc: c.addAPIService,
UpdateFunc: c.updateAPIService,
DeleteFunc: c.deleteAPIService,
})
c.syncFn = c.sync
return c
}
func (c *APIServiceRegistrationController) sync(key string) error {
apiServer, err := c.apiServerLister.Get(key)
if apierrors.IsNotFound(err) {
c.apiHandlerManager.RemoveAPIService(key)
return nil
}
if err != nil {
return err
}
c.apiHandlerManager.AddAPIService(apiServer)
return nil
}
func (c *APIServiceRegistrationController) Run(stopCh <-chan struct{}) {
defer utilruntime.HandleCrash()
defer c.queue.ShutDown()
defer glog.Infof("Shutting down APIServiceRegistrationController")
glog.Infof("Starting APIServiceRegistrationController")
// only start one worker thread since its a slow moving API and the discovery server adding bits
// aren't threadsafe
go wait.Until(c.runWorker, time.Second, stopCh)
<-stopCh
}
func (c *APIServiceRegistrationController) runWorker() {
for c.processNextWorkItem() {
}
}
// processNextWorkItem deals with one key off the queue. It returns false when it's time to quit.
func (c *APIServiceRegistrationController) processNextWorkItem() bool {
key, quit := c.queue.Get()
if quit {
return false
}
defer c.queue.Done(key)
err := c.syncFn(key.(string))
if err == nil {
c.queue.Forget(key)
return true
}
utilruntime.HandleError(fmt.Errorf("%v failed with : %v", key, err))
c.queue.AddRateLimited(key)
return true
}
func (c *APIServiceRegistrationController) enqueue(obj *apiregistration.APIService) {
key, err := controller.KeyFunc(obj)
if err != nil {
glog.Errorf("Couldn't get key for object %#v: %v", obj, err)
return
}
c.queue.Add(key)
}
func (c *APIServiceRegistrationController) addAPIService(obj interface{}) {
castObj := obj.(*apiregistration.APIService)
glog.V(4).Infof("Adding %s", castObj.Name)
c.enqueue(castObj)
}
func (c *APIServiceRegistrationController) updateAPIService(obj, _ interface{}) {
castObj := obj.(*apiregistration.APIService)
glog.V(4).Infof("Updating %s", castObj.Name)
c.enqueue(castObj)
}
func (c *APIServiceRegistrationController) deleteAPIService(obj interface{}) {
castObj, ok := obj.(*apiregistration.APIService)
if !ok {
tombstone, ok := obj.(cache.DeletedFinalStateUnknown)
if !ok {
glog.Errorf("Couldn't get object from tombstone %#v", obj)
return
}
castObj, ok = tombstone.Obj.(*apiregistration.APIService)
if !ok {
glog.Errorf("Tombstone contained object that is not expected %#v", obj)
return
}
}
glog.V(4).Infof("Deleting %q", castObj.Name)
c.enqueue(castObj)
}

View File

@ -0,0 +1,177 @@
/*
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"
"strings"
"k8s.io/kubernetes/pkg/api"
apierrors "k8s.io/kubernetes/pkg/api/errors"
metav1 "k8s.io/kubernetes/pkg/apis/meta/v1"
"k8s.io/kubernetes/pkg/apiserver"
"k8s.io/kubernetes/pkg/labels"
"k8s.io/kubernetes/pkg/runtime"
apiregistrationapi "k8s.io/kubernetes/cmd/kubernetes-discovery/pkg/apis/apiregistration"
apiregistrationv1alpha1api "k8s.io/kubernetes/cmd/kubernetes-discovery/pkg/apis/apiregistration/v1alpha1"
informers "k8s.io/kubernetes/cmd/kubernetes-discovery/pkg/client/informers/apiregistration/internalversion"
listers "k8s.io/kubernetes/cmd/kubernetes-discovery/pkg/client/listers/apiregistration/internalversion"
)
// WithAPIs adds the handling for /apis and /apis/<group: -apiregistration.k8s.io>.
func WithAPIs(handler http.Handler, informer informers.APIServiceInformer) http.Handler {
apisHandler := &apisHandler{
lister: informer.Lister(),
delegate: handler,
}
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
apisHandler.ServeHTTP(w, req)
})
}
// apisHandler servers the `/apis` endpoint.
// This is registered as a filter so that it never collides with any explictly registered endpoints
type apisHandler struct {
lister listers.APIServiceLister
delegate http.Handler
}
var discoveryGroup = metav1.APIGroup{
Name: apiregistrationapi.GroupName,
Versions: []metav1.GroupVersionForDiscovery{
{
GroupVersion: apiregistrationv1alpha1api.SchemeGroupVersion.String(),
Version: apiregistrationv1alpha1api.SchemeGroupVersion.Version,
},
},
PreferredVersion: metav1.GroupVersionForDiscovery{
GroupVersion: apiregistrationv1alpha1api.SchemeGroupVersion.String(),
Version: apiregistrationv1alpha1api.SchemeGroupVersion.Version,
},
}
func (r *apisHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
// if the URL is for OUR api group, serve it normally
if strings.HasPrefix(req.URL.Path+"/", "/apis/"+apiregistrationapi.GroupName+"/") {
r.delegate.ServeHTTP(w, req)
return
}
// don't handle URLs that aren't /apis
if req.URL.Path != "/apis" && req.URL.Path != "/apis/" {
r.delegate.ServeHTTP(w, req)
return
}
discoveryGroupList := &metav1.APIGroupList{
// always add OUR api group to the list first
Groups: []metav1.APIGroup{discoveryGroup},
}
apiServices, err := r.lister.List(labels.Everything())
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
apiServicesByGroup := apiregistrationapi.SortedByGroup(apiServices)
for _, apiGroupServers := range apiServicesByGroup {
// skip the legacy group
if len(apiGroupServers[0].Spec.Group) == 0 {
continue
}
discoveryGroupList.Groups = append(discoveryGroupList.Groups, *newDiscoveryAPIGroup(apiGroupServers))
}
json, err := runtime.Encode(api.Codecs.LegacyCodec(), discoveryGroupList)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
if _, err := w.Write(json); err != nil {
panic(err)
}
}
func newDiscoveryAPIGroup(apiServices []*apiregistrationapi.APIService) *metav1.APIGroup {
apiServicesByGroup := apiregistrationapi.SortedByGroup(apiServices)[0]
discoveryGroup := &metav1.APIGroup{
Name: apiServicesByGroup[0].Spec.Group,
PreferredVersion: metav1.GroupVersionForDiscovery{
GroupVersion: apiServicesByGroup[0].Spec.Group + "/" + apiServicesByGroup[0].Spec.Version,
Version: apiServicesByGroup[0].Spec.Version,
},
}
for _, apiService := range apiServicesByGroup {
discoveryGroup.Versions = append(discoveryGroup.Versions,
metav1.GroupVersionForDiscovery{
GroupVersion: apiService.Spec.Group + "/" + apiService.Spec.Version,
Version: apiService.Spec.Version,
},
)
}
return discoveryGroup
}
// apiGroupHandler servers the `/apis/<group>` endpoint.
type apiGroupHandler struct {
groupName string
lister listers.APIServiceLister
}
func (r *apiGroupHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
// don't handle URLs that aren't /apis/<groupName>
if req.URL.Path != "/apis/"+r.groupName && req.URL.Path != "/apis/"+r.groupName+"/" {
http.Error(w, "", http.StatusNotFound)
return
}
apiServices, err := r.lister.List(labels.Everything())
if statusErr, ok := err.(*apierrors.StatusError); ok && err != nil {
apiserver.WriteRawJSON(int(statusErr.Status().Code), statusErr.Status(), w)
return
}
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
apiServicesForGroup := []*apiregistrationapi.APIService{}
for _, apiService := range apiServices {
if apiService.Spec.Group == r.groupName {
apiServicesForGroup = append(apiServicesForGroup, apiService)
}
}
if len(apiServicesForGroup) == 0 {
http.Error(w, "", http.StatusNotFound)
return
}
json, err := runtime.Encode(api.Codecs.LegacyCodec(), newDiscoveryAPIGroup(apiServicesForGroup))
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
if _, err := w.Write(json); err != nil {
panic(err)
}
}

View File

@ -0,0 +1,409 @@
/*
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"
"testing"
"k8s.io/kubernetes/pkg/api"
metav1 "k8s.io/kubernetes/pkg/apis/meta/v1"
"k8s.io/kubernetes/pkg/client/cache"
"k8s.io/kubernetes/pkg/runtime"
"k8s.io/kubernetes/pkg/util/diff"
"k8s.io/kubernetes/cmd/kubernetes-discovery/pkg/apis/apiregistration"
listers "k8s.io/kubernetes/cmd/kubernetes-discovery/pkg/client/listers/apiregistration/internalversion"
)
type delegationHTTPHandler struct {
called bool
}
func (d *delegationHTTPHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
d.called = true
w.WriteHeader(http.StatusOK)
}
func TestAPIsDelegation(t *testing.T) {
indexer := cache.NewIndexer(cache.MetaNamespaceKeyFunc, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc})
delegate := &delegationHTTPHandler{}
handler := &apisHandler{
lister: listers.NewAPIServiceLister(indexer),
delegate: delegate,
}
server := httptest.NewServer(handler)
defer server.Close()
pathToDelegation := map[string]bool{
"/": true,
"/apis": false,
"/apis/": false,
"/apis/" + apiregistration.GroupName: true,
"/apis/" + apiregistration.GroupName + "/": true,
"/apis/" + apiregistration.GroupName + "/anything": true,
"/apis/" + apiregistration.GroupName + "/anything/again": true,
"/apis/something": true,
"/apis/something/nested": true,
"/apis/something/nested/deeper": true,
"/api": true,
"/api/v1": true,
"/version": true,
}
for path, expectedDelegation := range pathToDelegation {
delegate.called = false
resp, err := http.Get(server.URL + path)
if err != nil {
t.Errorf("%s: %v", path, err)
continue
}
if resp.StatusCode != http.StatusOK {
httputil.DumpResponse(resp, true)
t.Errorf("%s: %v", path, err)
continue
}
if e, a := expectedDelegation, delegate.called; e != a {
t.Errorf("%s: expected %v, got %v", path, e, a)
continue
}
}
}
func TestAPIs(t *testing.T) {
tests := []struct {
name string
apiservices []*apiregistration.APIService
expected *metav1.APIGroupList
}{
{
name: "empty",
apiservices: []*apiregistration.APIService{},
expected: &metav1.APIGroupList{
TypeMeta: metav1.TypeMeta{Kind: "APIGroupList", APIVersion: "v1"},
Groups: []metav1.APIGroup{
discoveryGroup,
},
},
},
{
name: "simple add",
apiservices: []*apiregistration.APIService{
{
ObjectMeta: api.ObjectMeta{Name: "v1.foo"},
Spec: apiregistration.APIServiceSpec{
Group: "foo",
Version: "v1",
Priority: 10,
},
},
{
ObjectMeta: api.ObjectMeta{Name: "v1.bar"},
Spec: apiregistration.APIServiceSpec{
Group: "bar",
Version: "v1",
Priority: 11,
},
},
},
expected: &metav1.APIGroupList{
TypeMeta: metav1.TypeMeta{Kind: "APIGroupList", APIVersion: "v1"},
Groups: []metav1.APIGroup{
discoveryGroup,
{
Name: "foo",
Versions: []metav1.GroupVersionForDiscovery{
{
GroupVersion: "foo/v1",
Version: "v1",
},
},
PreferredVersion: metav1.GroupVersionForDiscovery{
GroupVersion: "foo/v1",
Version: "v1",
},
},
{
Name: "bar",
Versions: []metav1.GroupVersionForDiscovery{
{
GroupVersion: "bar/v1",
Version: "v1",
},
},
PreferredVersion: metav1.GroupVersionForDiscovery{
GroupVersion: "bar/v1",
Version: "v1",
},
},
},
},
},
{
name: "sorting",
apiservices: []*apiregistration.APIService{
{
ObjectMeta: api.ObjectMeta{Name: "v1.foo"},
Spec: apiregistration.APIServiceSpec{
Group: "foo",
Version: "v1",
Priority: 20,
},
},
{
ObjectMeta: api.ObjectMeta{Name: "v2.bar"},
Spec: apiregistration.APIServiceSpec{
Group: "bar",
Version: "v2",
Priority: 11,
},
},
{
ObjectMeta: api.ObjectMeta{Name: "v2.foo"},
Spec: apiregistration.APIServiceSpec{
Group: "foo",
Version: "v2",
Priority: 1,
},
},
{
ObjectMeta: api.ObjectMeta{Name: "v1.bar"},
Spec: apiregistration.APIServiceSpec{
Group: "bar",
Version: "v1",
Priority: 11,
},
},
},
expected: &metav1.APIGroupList{
TypeMeta: metav1.TypeMeta{Kind: "APIGroupList", APIVersion: "v1"},
Groups: []metav1.APIGroup{
discoveryGroup,
{
Name: "foo",
Versions: []metav1.GroupVersionForDiscovery{
{
GroupVersion: "foo/v2",
Version: "v2",
},
{
GroupVersion: "foo/v1",
Version: "v1",
},
},
PreferredVersion: metav1.GroupVersionForDiscovery{
GroupVersion: "foo/v2",
Version: "v2",
},
},
{
Name: "bar",
Versions: []metav1.GroupVersionForDiscovery{
{
GroupVersion: "bar/v1",
Version: "v1",
},
{
GroupVersion: "bar/v2",
Version: "v2",
},
},
PreferredVersion: metav1.GroupVersionForDiscovery{
GroupVersion: "bar/v1",
Version: "v1",
},
},
},
},
},
}
for _, tc := range tests {
indexer := cache.NewIndexer(cache.MetaNamespaceKeyFunc, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc})
delegate := &delegationHTTPHandler{}
handler := &apisHandler{
lister: listers.NewAPIServiceLister(indexer),
delegate: delegate,
}
for _, o := range tc.apiservices {
indexer.Add(o)
}
server := httptest.NewServer(handler)
defer server.Close()
resp, err := http.Get(server.URL + "/apis")
if err != nil {
t.Errorf("%s: %v", tc.name, err)
continue
}
bytes, err := ioutil.ReadAll(resp.Body)
if err != nil {
t.Errorf("%s: %v", tc.name, err)
continue
}
actual := &metav1.APIGroupList{}
if err := runtime.DecodeInto(api.Codecs.UniversalDecoder(), bytes, actual); err != nil {
t.Errorf("%s: %v", tc.name, err)
continue
}
if !api.Semantic.DeepEqual(tc.expected, actual) {
t.Errorf("%s: %v", tc.name, diff.ObjectDiff(tc.expected, actual))
continue
}
}
}
func TestAPIGroupMissing(t *testing.T) {
indexer := cache.NewIndexer(cache.MetaNamespaceKeyFunc, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc})
handler := &apiGroupHandler{
lister: listers.NewAPIServiceLister(indexer),
groupName: "foo",
}
server := httptest.NewServer(handler)
defer server.Close()
resp, err := http.Get(server.URL + "/apis/groupName/foo")
if err != nil {
t.Fatal(err)
}
if resp.StatusCode != http.StatusNotFound {
t.Fatalf("expected %v, got %v", resp.StatusCode, http.StatusNotFound)
}
// foo still has no api services for it (like it was deleted), it should 404
resp, err = http.Get(server.URL + "/apis/groupName/")
if err != nil {
t.Fatal(err)
}
if resp.StatusCode != http.StatusNotFound {
t.Fatalf("expected %v, got %v", resp.StatusCode, http.StatusNotFound)
}
}
func TestAPIGroup(t *testing.T) {
tests := []struct {
name string
group string
apiservices []*apiregistration.APIService
expected *metav1.APIGroup
}{
{
name: "sorting",
group: "foo",
apiservices: []*apiregistration.APIService{
{
ObjectMeta: api.ObjectMeta{Name: "v1.foo"},
Spec: apiregistration.APIServiceSpec{
Group: "foo",
Version: "v1",
Priority: 20,
},
},
{
ObjectMeta: api.ObjectMeta{Name: "v2.bar"},
Spec: apiregistration.APIServiceSpec{
Group: "bar",
Version: "v2",
Priority: 11,
},
},
{
ObjectMeta: api.ObjectMeta{Name: "v2.foo"},
Spec: apiregistration.APIServiceSpec{
Group: "foo",
Version: "v2",
Priority: 1,
},
},
{
ObjectMeta: api.ObjectMeta{Name: "v1.bar"},
Spec: apiregistration.APIServiceSpec{
Group: "bar",
Version: "v1",
Priority: 11,
},
},
},
expected: &metav1.APIGroup{
TypeMeta: metav1.TypeMeta{Kind: "APIGroup", APIVersion: "v1"},
Name: "foo",
Versions: []metav1.GroupVersionForDiscovery{
{
GroupVersion: "foo/v2",
Version: "v2",
},
{
GroupVersion: "foo/v1",
Version: "v1",
},
},
PreferredVersion: metav1.GroupVersionForDiscovery{
GroupVersion: "foo/v2",
Version: "v2",
},
},
},
}
for _, tc := range tests {
indexer := cache.NewIndexer(cache.MetaNamespaceKeyFunc, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc})
handler := &apiGroupHandler{
lister: listers.NewAPIServiceLister(indexer),
groupName: "foo",
}
for _, o := range tc.apiservices {
indexer.Add(o)
}
server := httptest.NewServer(handler)
defer server.Close()
resp, err := http.Get(server.URL + "/apis/" + tc.group)
if err != nil {
t.Errorf("%s: %v", tc.name, err)
continue
}
if resp.StatusCode != http.StatusOK {
httputil.DumpResponse(resp, true)
t.Errorf("%s", tc.name)
continue
}
bytes, err := ioutil.ReadAll(resp.Body)
if err != nil {
t.Errorf("%s: %v", tc.name, err)
continue
}
actual := &metav1.APIGroup{}
if err := runtime.DecodeInto(api.Codecs.UniversalDecoder(), bytes, actual); err != nil {
t.Errorf("%s: %v", tc.name, err)
continue
}
if !api.Semantic.DeepEqual(tc.expected, actual) {
t.Errorf("%s: %v", tc.name, diff.ObjectDiff(tc.expected, actual))
continue
}
}
}

View File

@ -538,6 +538,9 @@ function start_discovery {
return
fi
${CONTROLPLANE_SUDO} cp "${CERT_DIR}/admin.kubeconfig" "${CERT_DIR}/admin-discovery.kubeconfig"
${CONTROLPLANE_SUDO} ${GO_OUT}/kubectl config set-cluster local-up-cluster --kubeconfig="${CERT_DIR}/admin-discovery.kubeconfig" --insecure-skip-tls-verify --server="https://${API_HOST}:${DISCOVERY_SECURE_PORT}"
DISCOVERY_SERVER_LOG=/tmp/kubernetes-discovery.log
${CONTROLPLANE_SUDO} "${GO_OUT}/kubernetes-discovery" \
--cert-dir="${CERT_DIR}" \
@ -558,6 +561,9 @@ function start_discovery {
# Wait for kubernetes-discovery to come up before launching the rest of the components.
echo "Waiting for kubernetes-discovery to come up"
kube::util::wait_for_url "https://${API_HOST}:${DISCOVERY_SECURE_PORT}/version" "kubernetes-discovery: " 1 ${WAIT_FOR_URL_API_SERVER} || exit 1
# create the "normal" api services for the core API server
${CONTROLPLANE_SUDO} ${GO_OUT}/kubectl create -f "${KUBE_ROOT}/cmd/kubernetes-discovery/artifacts/core-apiservices" --kubeconfig="${CERT_DIR}/admin-discovery.kubeconfig"
}
@ -832,7 +838,6 @@ if [[ "${START_MODE}" != "kubeletonly" ]]; then
start_etcd
set_service_accounts
start_apiserver
start_discovery
start_controller_manager
start_kubeproxy
start_kubedns
@ -842,6 +847,11 @@ if [[ "${START_MODE}" != "nokubelet" ]]; then
start_kubelet
fi
START_DISCOVERY=${START_DISCOVERY:-false}
if [[ "${START_DISCOVERY}" = true ]]; then
start_discovery
fi
print_success
if [[ "${ENABLE_DAEMON}" = false ]]; then