2019-01-12 04:58:27 +00:00
|
|
|
/*
|
|
|
|
Copyright 2017 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"
|
|
|
|
"sort"
|
|
|
|
"time"
|
|
|
|
|
|
|
|
"k8s.io/klog"
|
|
|
|
|
|
|
|
autoscaling "k8s.io/api/autoscaling/v1"
|
|
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
|
|
"k8s.io/apimachinery/pkg/labels"
|
|
|
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
|
|
|
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
|
|
|
|
"k8s.io/apimachinery/pkg/util/wait"
|
|
|
|
"k8s.io/apimachinery/pkg/version"
|
|
|
|
"k8s.io/apiserver/pkg/endpoints/discovery"
|
|
|
|
"k8s.io/client-go/tools/cache"
|
|
|
|
"k8s.io/client-go/util/workqueue"
|
|
|
|
|
2020-03-26 21:07:15 +00:00
|
|
|
apiextensionshelpers "k8s.io/apiextensions-apiserver/pkg/apihelpers"
|
|
|
|
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
|
|
|
|
informers "k8s.io/apiextensions-apiserver/pkg/client/informers/externalversions/apiextensions/v1"
|
|
|
|
listers "k8s.io/apiextensions-apiserver/pkg/client/listers/apiextensions/v1"
|
2019-01-12 04:58:27 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
type DiscoveryController struct {
|
|
|
|
versionHandler *versionDiscoveryHandler
|
|
|
|
groupHandler *groupDiscoveryHandler
|
|
|
|
|
|
|
|
crdLister listers.CustomResourceDefinitionLister
|
|
|
|
crdsSynced cache.InformerSynced
|
|
|
|
|
|
|
|
// To allow injection for testing.
|
|
|
|
syncFn func(version schema.GroupVersion) error
|
|
|
|
|
|
|
|
queue workqueue.RateLimitingInterface
|
|
|
|
}
|
|
|
|
|
|
|
|
func NewDiscoveryController(crdInformer informers.CustomResourceDefinitionInformer, versionHandler *versionDiscoveryHandler, groupHandler *groupDiscoveryHandler) *DiscoveryController {
|
|
|
|
c := &DiscoveryController{
|
|
|
|
versionHandler: versionHandler,
|
|
|
|
groupHandler: groupHandler,
|
|
|
|
crdLister: crdInformer.Lister(),
|
|
|
|
crdsSynced: crdInformer.Informer().HasSynced,
|
|
|
|
|
|
|
|
queue: workqueue.NewNamedRateLimitingQueue(workqueue.DefaultControllerRateLimiter(), "DiscoveryController"),
|
|
|
|
}
|
|
|
|
|
|
|
|
crdInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{
|
|
|
|
AddFunc: c.addCustomResourceDefinition,
|
|
|
|
UpdateFunc: c.updateCustomResourceDefinition,
|
|
|
|
DeleteFunc: c.deleteCustomResourceDefinition,
|
|
|
|
})
|
|
|
|
|
|
|
|
c.syncFn = c.sync
|
|
|
|
|
|
|
|
return c
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c *DiscoveryController) sync(version schema.GroupVersion) error {
|
|
|
|
|
|
|
|
apiVersionsForDiscovery := []metav1.GroupVersionForDiscovery{}
|
|
|
|
apiResourcesForDiscovery := []metav1.APIResource{}
|
|
|
|
versionsForDiscoveryMap := map[metav1.GroupVersion]bool{}
|
|
|
|
|
|
|
|
crds, err := c.crdLister.List(labels.Everything())
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
foundVersion := false
|
|
|
|
foundGroup := false
|
|
|
|
for _, crd := range crds {
|
2020-03-26 21:07:15 +00:00
|
|
|
if !apiextensionshelpers.IsCRDConditionTrue(crd, apiextensionsv1.Established) {
|
2019-01-12 04:58:27 +00:00
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
if crd.Spec.Group != version.Group {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
foundThisVersion := false
|
2019-08-30 18:33:25 +00:00
|
|
|
var storageVersionHash string
|
2019-01-12 04:58:27 +00:00
|
|
|
for _, v := range crd.Spec.Versions {
|
|
|
|
if !v.Served {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
// If there is any Served version, that means the group should show up in discovery
|
|
|
|
foundGroup = true
|
|
|
|
|
|
|
|
gv := metav1.GroupVersion{Group: crd.Spec.Group, Version: v.Name}
|
|
|
|
if !versionsForDiscoveryMap[gv] {
|
|
|
|
versionsForDiscoveryMap[gv] = true
|
|
|
|
apiVersionsForDiscovery = append(apiVersionsForDiscovery, metav1.GroupVersionForDiscovery{
|
|
|
|
GroupVersion: crd.Spec.Group + "/" + v.Name,
|
|
|
|
Version: v.Name,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
if v.Name == version.Version {
|
|
|
|
foundThisVersion = true
|
|
|
|
}
|
2019-08-30 18:33:25 +00:00
|
|
|
if v.Storage {
|
|
|
|
storageVersionHash = discovery.StorageVersionHash(gv.Group, gv.Version, crd.Spec.Names.Kind)
|
|
|
|
}
|
2019-01-12 04:58:27 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if !foundThisVersion {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
foundVersion = true
|
|
|
|
|
|
|
|
verbs := metav1.Verbs([]string{"delete", "deletecollection", "get", "list", "patch", "create", "update", "watch"})
|
|
|
|
// if we're terminating we don't allow some verbs
|
2020-03-26 21:07:15 +00:00
|
|
|
if apiextensionshelpers.IsCRDConditionTrue(crd, apiextensionsv1.Terminating) {
|
2019-01-12 04:58:27 +00:00
|
|
|
verbs = metav1.Verbs([]string{"delete", "deletecollection", "get", "list", "watch"})
|
|
|
|
}
|
|
|
|
|
|
|
|
apiResourcesForDiscovery = append(apiResourcesForDiscovery, metav1.APIResource{
|
2019-04-07 17:07:55 +00:00
|
|
|
Name: crd.Status.AcceptedNames.Plural,
|
|
|
|
SingularName: crd.Status.AcceptedNames.Singular,
|
2020-03-26 21:07:15 +00:00
|
|
|
Namespaced: crd.Spec.Scope == apiextensionsv1.NamespaceScoped,
|
2019-04-07 17:07:55 +00:00
|
|
|
Kind: crd.Status.AcceptedNames.Kind,
|
|
|
|
Verbs: verbs,
|
|
|
|
ShortNames: crd.Status.AcceptedNames.ShortNames,
|
|
|
|
Categories: crd.Status.AcceptedNames.Categories,
|
2019-08-30 18:33:25 +00:00
|
|
|
StorageVersionHash: storageVersionHash,
|
2019-01-12 04:58:27 +00:00
|
|
|
})
|
|
|
|
|
2020-03-26 21:07:15 +00:00
|
|
|
subresources, err := apiextensionshelpers.GetSubresourcesForVersion(crd, version.Version)
|
2019-01-12 04:58:27 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
if subresources != nil && subresources.Status != nil {
|
|
|
|
apiResourcesForDiscovery = append(apiResourcesForDiscovery, metav1.APIResource{
|
|
|
|
Name: crd.Status.AcceptedNames.Plural + "/status",
|
2020-03-26 21:07:15 +00:00
|
|
|
Namespaced: crd.Spec.Scope == apiextensionsv1.NamespaceScoped,
|
2019-01-12 04:58:27 +00:00
|
|
|
Kind: crd.Status.AcceptedNames.Kind,
|
|
|
|
Verbs: metav1.Verbs([]string{"get", "patch", "update"}),
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
if subresources != nil && subresources.Scale != nil {
|
|
|
|
apiResourcesForDiscovery = append(apiResourcesForDiscovery, metav1.APIResource{
|
|
|
|
Group: autoscaling.GroupName,
|
|
|
|
Version: "v1",
|
|
|
|
Kind: "Scale",
|
|
|
|
Name: crd.Status.AcceptedNames.Plural + "/scale",
|
2020-03-26 21:07:15 +00:00
|
|
|
Namespaced: crd.Spec.Scope == apiextensionsv1.NamespaceScoped,
|
2019-01-12 04:58:27 +00:00
|
|
|
Verbs: metav1.Verbs([]string{"get", "patch", "update"}),
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if !foundGroup {
|
|
|
|
c.groupHandler.unsetDiscovery(version.Group)
|
|
|
|
c.versionHandler.unsetDiscovery(version)
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
sortGroupDiscoveryByKubeAwareVersion(apiVersionsForDiscovery)
|
|
|
|
|
|
|
|
apiGroup := metav1.APIGroup{
|
|
|
|
Name: version.Group,
|
|
|
|
Versions: apiVersionsForDiscovery,
|
|
|
|
// the preferred versions for a group is the first item in
|
|
|
|
// apiVersionsForDiscovery after it put in the right ordered
|
|
|
|
PreferredVersion: apiVersionsForDiscovery[0],
|
|
|
|
}
|
|
|
|
c.groupHandler.setDiscovery(version.Group, discovery.NewAPIGroupHandler(Codecs, apiGroup))
|
|
|
|
|
|
|
|
if !foundVersion {
|
|
|
|
c.versionHandler.unsetDiscovery(version)
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
c.versionHandler.setDiscovery(version, discovery.NewAPIVersionHandler(Codecs, version, discovery.APIResourceListerFunc(func() []metav1.APIResource {
|
|
|
|
return apiResourcesForDiscovery
|
|
|
|
})))
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func sortGroupDiscoveryByKubeAwareVersion(gd []metav1.GroupVersionForDiscovery) {
|
|
|
|
sort.Slice(gd, func(i, j int) bool {
|
|
|
|
return version.CompareKubeAwareVersionStrings(gd[i].Version, gd[j].Version) > 0
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c *DiscoveryController) Run(stopCh <-chan struct{}) {
|
|
|
|
defer utilruntime.HandleCrash()
|
|
|
|
defer c.queue.ShutDown()
|
|
|
|
defer klog.Infof("Shutting down DiscoveryController")
|
|
|
|
|
|
|
|
klog.Infof("Starting DiscoveryController")
|
|
|
|
|
|
|
|
if !cache.WaitForCacheSync(stopCh, c.crdsSynced) {
|
|
|
|
utilruntime.HandleError(fmt.Errorf("timed out waiting for caches to sync"))
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// only start one worker thread since its a slow moving API
|
|
|
|
go wait.Until(c.runWorker, time.Second, stopCh)
|
|
|
|
|
|
|
|
<-stopCh
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c *DiscoveryController) runWorker() {
|
|
|
|
for c.processNextWorkItem() {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// processNextWorkItem deals with one key off the queue. It returns false when it's time to quit.
|
|
|
|
func (c *DiscoveryController) processNextWorkItem() bool {
|
|
|
|
key, quit := c.queue.Get()
|
|
|
|
if quit {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
defer c.queue.Done(key)
|
|
|
|
|
|
|
|
err := c.syncFn(key.(schema.GroupVersion))
|
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2020-03-26 21:07:15 +00:00
|
|
|
func (c *DiscoveryController) enqueue(obj *apiextensionsv1.CustomResourceDefinition) {
|
2019-01-12 04:58:27 +00:00
|
|
|
for _, v := range obj.Spec.Versions {
|
|
|
|
c.queue.Add(schema.GroupVersion{Group: obj.Spec.Group, Version: v.Name})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c *DiscoveryController) addCustomResourceDefinition(obj interface{}) {
|
2020-03-26 21:07:15 +00:00
|
|
|
castObj := obj.(*apiextensionsv1.CustomResourceDefinition)
|
2019-01-12 04:58:27 +00:00
|
|
|
klog.V(4).Infof("Adding customresourcedefinition %s", castObj.Name)
|
|
|
|
c.enqueue(castObj)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c *DiscoveryController) updateCustomResourceDefinition(oldObj, newObj interface{}) {
|
2020-03-26 21:07:15 +00:00
|
|
|
castNewObj := newObj.(*apiextensionsv1.CustomResourceDefinition)
|
|
|
|
castOldObj := oldObj.(*apiextensionsv1.CustomResourceDefinition)
|
2019-01-12 04:58:27 +00:00
|
|
|
klog.V(4).Infof("Updating customresourcedefinition %s", castOldObj.Name)
|
|
|
|
// Enqueue both old and new object to make sure we remove and add appropriate Versions.
|
|
|
|
// The working queue will resolve any duplicates and only changes will stay in the queue.
|
|
|
|
c.enqueue(castNewObj)
|
|
|
|
c.enqueue(castOldObj)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c *DiscoveryController) deleteCustomResourceDefinition(obj interface{}) {
|
2020-03-26 21:07:15 +00:00
|
|
|
castObj, ok := obj.(*apiextensionsv1.CustomResourceDefinition)
|
2019-01-12 04:58:27 +00:00
|
|
|
if !ok {
|
|
|
|
tombstone, ok := obj.(cache.DeletedFinalStateUnknown)
|
|
|
|
if !ok {
|
|
|
|
klog.Errorf("Couldn't get object from tombstone %#v", obj)
|
|
|
|
return
|
|
|
|
}
|
2020-03-26 21:07:15 +00:00
|
|
|
castObj, ok = tombstone.Obj.(*apiextensionsv1.CustomResourceDefinition)
|
2019-01-12 04:58:27 +00:00
|
|
|
if !ok {
|
|
|
|
klog.Errorf("Tombstone contained object that is not expected %#v", obj)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
|
klog.V(4).Infof("Deleting customresourcedefinition %q", castObj.Name)
|
|
|
|
c.enqueue(castObj)
|
|
|
|
}
|