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 finalizer
import (
2020-03-26 21:07:15 +00:00
"context"
2019-01-12 04:58:27 +00:00
"fmt"
"reflect"
"time"
2020-08-10 17:43:49 +00:00
"k8s.io/klog/v2"
2019-01-12 04:58:27 +00:00
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/api/meta"
2020-03-26 21:07:15 +00:00
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
2019-01-12 04:58:27 +00:00
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
2019-09-27 21:51:53 +00:00
"k8s.io/apimachinery/pkg/runtime/schema"
2019-01-12 04:58:27 +00:00
utilerrors "k8s.io/apimachinery/pkg/util/errors"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/apimachinery/pkg/util/wait"
genericapirequest "k8s.io/apiserver/pkg/endpoints/request"
"k8s.io/apiserver/pkg/registry/rest"
"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"
client "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/typed/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
)
2019-09-27 21:51:53 +00:00
// OverlappingBuiltInResources returns the set of built-in group/resources that are persisted
// in storage paths that overlap with CRD storage paths, and should not be deleted
// by this controller if an associated CRD is deleted.
func OverlappingBuiltInResources ( ) map [ schema . GroupResource ] bool {
return map [ schema . GroupResource ] bool {
{ Group : "apiregistration.k8s.io" , Resource : "apiservices" } : true ,
{ Group : "apiextensions.k8s.io" , Resource : "customresourcedefinitions" } : true ,
}
}
2019-01-12 04:58:27 +00:00
// CRDFinalizer is a controller that finalizes the CRD by deleting all the CRs associated with it.
type CRDFinalizer struct {
crdClient client . CustomResourceDefinitionsGetter
crClientGetter CRClientGetter
crdLister listers . CustomResourceDefinitionLister
crdSynced cache . InformerSynced
// To allow injection for testing.
syncFn func ( key string ) error
queue workqueue . RateLimitingInterface
}
// ListerCollectionDeleter combines rest.Lister and rest.CollectionDeleter.
type ListerCollectionDeleter interface {
rest . Lister
rest . CollectionDeleter
}
// CRClientGetter knows how to get a ListerCollectionDeleter for a given CRD UID.
type CRClientGetter interface {
// GetCustomResourceListerCollectionDeleter gets the ListerCollectionDeleter for the given CRD
// UID.
2020-03-26 21:07:15 +00:00
GetCustomResourceListerCollectionDeleter ( crd * apiextensionsv1 . CustomResourceDefinition ) ( ListerCollectionDeleter , error )
2019-01-12 04:58:27 +00:00
}
// NewCRDFinalizer creates a new CRDFinalizer.
func NewCRDFinalizer (
crdInformer informers . CustomResourceDefinitionInformer ,
crdClient client . CustomResourceDefinitionsGetter ,
crClientGetter CRClientGetter ,
) * CRDFinalizer {
c := & CRDFinalizer {
crdClient : crdClient ,
crdLister : crdInformer . Lister ( ) ,
crdSynced : crdInformer . Informer ( ) . HasSynced ,
crClientGetter : crClientGetter ,
2019-04-07 17:07:55 +00:00
queue : workqueue . NewNamedRateLimitingQueue ( workqueue . DefaultControllerRateLimiter ( ) , "crd_finalizer" ) ,
2019-01-12 04:58:27 +00:00
}
crdInformer . Informer ( ) . AddEventHandler ( cache . ResourceEventHandlerFuncs {
AddFunc : c . addCustomResourceDefinition ,
UpdateFunc : c . updateCustomResourceDefinition ,
} )
c . syncFn = c . sync
return c
}
func ( c * CRDFinalizer ) sync ( key string ) error {
cachedCRD , err := c . crdLister . Get ( key )
if apierrors . IsNotFound ( err ) {
return nil
}
if err != nil {
return err
}
// no work to do
2020-03-26 21:07:15 +00:00
if cachedCRD . DeletionTimestamp . IsZero ( ) || ! apiextensionshelpers . CRDHasFinalizer ( cachedCRD , apiextensionsv1 . CustomResourceCleanupFinalizer ) {
2019-01-12 04:58:27 +00:00
return nil
}
crd := cachedCRD . DeepCopy ( )
// update the status condition. This cleanup could take a while.
2020-03-26 21:07:15 +00:00
apiextensionshelpers . SetCRDCondition ( crd , apiextensionsv1 . CustomResourceDefinitionCondition {
Type : apiextensionsv1 . Terminating ,
Status : apiextensionsv1 . ConditionTrue ,
2019-01-12 04:58:27 +00:00
Reason : "InstanceDeletionInProgress" ,
Message : "CustomResource deletion is in progress" ,
} )
2020-03-26 21:07:15 +00:00
crd , err = c . crdClient . CustomResourceDefinitions ( ) . UpdateStatus ( context . TODO ( ) , crd , metav1 . UpdateOptions { } )
2019-08-30 18:33:25 +00:00
if apierrors . IsNotFound ( err ) || apierrors . IsConflict ( err ) {
// deleted or changed in the meantime, we'll get called again
return nil
}
2019-01-12 04:58:27 +00:00
if err != nil {
return err
}
// Now we can start deleting items. We should use the REST API to ensure that all normal admission runs.
// Since we control the endpoints, we know that delete collection works. No need to delete if not established.
2019-09-27 21:51:53 +00:00
if OverlappingBuiltInResources ( ) [ schema . GroupResource { Group : crd . Spec . Group , Resource : crd . Spec . Names . Plural } ] {
// Skip deletion, explain why, and proceed to remove the finalizer and delete the CRD
2020-03-26 21:07:15 +00:00
apiextensionshelpers . SetCRDCondition ( crd , apiextensionsv1 . CustomResourceDefinitionCondition {
Type : apiextensionsv1 . Terminating ,
Status : apiextensionsv1 . ConditionFalse ,
2019-09-27 21:51:53 +00:00
Reason : "OverlappingBuiltInResource" ,
Message : "instances overlap with built-in resources in storage" ,
} )
2020-03-26 21:07:15 +00:00
} else if apiextensionshelpers . IsCRDConditionTrue ( crd , apiextensionsv1 . Established ) {
2019-01-12 04:58:27 +00:00
cond , deleteErr := c . deleteInstances ( crd )
2020-03-26 21:07:15 +00:00
apiextensionshelpers . SetCRDCondition ( crd , cond )
2019-01-12 04:58:27 +00:00
if deleteErr != nil {
2020-03-26 21:07:15 +00:00
if _ , err = c . crdClient . CustomResourceDefinitions ( ) . UpdateStatus ( context . TODO ( ) , crd , metav1 . UpdateOptions { } ) ; err != nil {
2019-01-12 04:58:27 +00:00
utilruntime . HandleError ( err )
}
return deleteErr
}
} else {
2020-03-26 21:07:15 +00:00
apiextensionshelpers . SetCRDCondition ( crd , apiextensionsv1 . CustomResourceDefinitionCondition {
Type : apiextensionsv1 . Terminating ,
Status : apiextensionsv1 . ConditionFalse ,
2019-01-12 04:58:27 +00:00
Reason : "NeverEstablished" ,
Message : "resource was never established" ,
} )
}
2020-03-26 21:07:15 +00:00
apiextensionshelpers . CRDRemoveFinalizer ( crd , apiextensionsv1 . CustomResourceCleanupFinalizer )
_ , err = c . crdClient . CustomResourceDefinitions ( ) . UpdateStatus ( context . TODO ( ) , crd , metav1 . UpdateOptions { } )
2019-08-30 18:33:25 +00:00
if apierrors . IsNotFound ( err ) || apierrors . IsConflict ( err ) {
// deleted or changed in the meantime, we'll get called again
return nil
}
return err
2019-01-12 04:58:27 +00:00
}
2020-03-26 21:07:15 +00:00
func ( c * CRDFinalizer ) deleteInstances ( crd * apiextensionsv1 . CustomResourceDefinition ) ( apiextensionsv1 . CustomResourceDefinitionCondition , error ) {
2019-01-12 04:58:27 +00:00
// Now we can start deleting items. While it would be ideal to use a REST API client, doing so
// could incorrectly delete a ThirdPartyResource with the same URL as the CustomResource, so we go
// directly to the storage instead. Since we control the storage, we know that delete collection works.
crClient , err := c . crClientGetter . GetCustomResourceListerCollectionDeleter ( crd )
if err != nil {
err = fmt . Errorf ( "unable to find a custom resource client for %s.%s: %v" , crd . Status . AcceptedNames . Plural , crd . Spec . Group , err )
2020-03-26 21:07:15 +00:00
return apiextensionsv1 . CustomResourceDefinitionCondition {
Type : apiextensionsv1 . Terminating ,
Status : apiextensionsv1 . ConditionTrue ,
2019-01-12 04:58:27 +00:00
Reason : "InstanceDeletionFailed" ,
Message : fmt . Sprintf ( "could not list instances: %v" , err ) ,
} , err
}
ctx := genericapirequest . NewContext ( )
allResources , err := crClient . List ( ctx , nil )
if err != nil {
2020-03-26 21:07:15 +00:00
return apiextensionsv1 . CustomResourceDefinitionCondition {
Type : apiextensionsv1 . Terminating ,
Status : apiextensionsv1 . ConditionTrue ,
2019-01-12 04:58:27 +00:00
Reason : "InstanceDeletionFailed" ,
Message : fmt . Sprintf ( "could not list instances: %v" , err ) ,
} , err
}
deletedNamespaces := sets . String { }
deleteErrors := [ ] error { }
for _ , item := range allResources . ( * unstructured . UnstructuredList ) . Items {
metadata , err := meta . Accessor ( & item )
if err != nil {
utilruntime . HandleError ( err )
continue
}
if deletedNamespaces . Has ( metadata . GetNamespace ( ) ) {
continue
}
// don't retry deleting the same namespace
deletedNamespaces . Insert ( metadata . GetNamespace ( ) )
nsCtx := genericapirequest . WithNamespace ( ctx , metadata . GetNamespace ( ) )
2019-08-30 18:33:25 +00:00
if _ , err := crClient . DeleteCollection ( nsCtx , rest . ValidateAllObjectFunc , nil , nil ) ; err != nil {
2019-01-12 04:58:27 +00:00
deleteErrors = append ( deleteErrors , err )
continue
}
}
if deleteError := utilerrors . NewAggregate ( deleteErrors ) ; deleteError != nil {
2020-03-26 21:07:15 +00:00
return apiextensionsv1 . CustomResourceDefinitionCondition {
Type : apiextensionsv1 . Terminating ,
Status : apiextensionsv1 . ConditionTrue ,
2019-01-12 04:58:27 +00:00
Reason : "InstanceDeletionFailed" ,
Message : fmt . Sprintf ( "could not issue all deletes: %v" , deleteError ) ,
} , deleteError
}
// now we need to wait until all the resources are deleted. Start with a simple poll before we do anything fancy.
// TODO not all servers are synchronized on caches. It is possible for a stale one to still be creating things.
// Once we have a mechanism for servers to indicate their states, we should check that for concurrence.
err = wait . PollImmediate ( 5 * time . Second , 1 * time . Minute , func ( ) ( bool , error ) {
listObj , err := crClient . List ( ctx , nil )
if err != nil {
return false , err
}
if len ( listObj . ( * unstructured . UnstructuredList ) . Items ) == 0 {
return true , nil
}
klog . V ( 2 ) . Infof ( "%s.%s waiting for %d items to be removed" , crd . Status . AcceptedNames . Plural , crd . Spec . Group , len ( listObj . ( * unstructured . UnstructuredList ) . Items ) )
return false , nil
} )
if err != nil {
2020-03-26 21:07:15 +00:00
return apiextensionsv1 . CustomResourceDefinitionCondition {
Type : apiextensionsv1 . Terminating ,
Status : apiextensionsv1 . ConditionTrue ,
2019-01-12 04:58:27 +00:00
Reason : "InstanceDeletionCheck" ,
Message : fmt . Sprintf ( "could not confirm zero CustomResources remaining: %v" , err ) ,
} , err
}
2020-03-26 21:07:15 +00:00
return apiextensionsv1 . CustomResourceDefinitionCondition {
Type : apiextensionsv1 . Terminating ,
Status : apiextensionsv1 . ConditionFalse ,
2019-01-12 04:58:27 +00:00
Reason : "InstanceDeletionCompleted" ,
Message : "removed all instances" ,
} , nil
}
func ( c * CRDFinalizer ) Run ( workers int , stopCh <- chan struct { } ) {
defer utilruntime . HandleCrash ( )
defer c . queue . ShutDown ( )
2021-03-18 22:40:29 +00:00
klog . Info ( "Starting CRDFinalizer" )
defer klog . Info ( "Shutting down CRDFinalizer" )
2019-01-12 04:58:27 +00:00
if ! cache . WaitForCacheSync ( stopCh , c . crdSynced ) {
return
}
for i := 0 ; i < workers ; i ++ {
go wait . Until ( c . runWorker , time . Second , stopCh )
}
<- stopCh
}
func ( c * CRDFinalizer ) runWorker ( ) {
for c . processNextWorkItem ( ) {
}
}
// processNextWorkItem deals with one key off the queue. It returns false when it's time to quit.
func ( c * CRDFinalizer ) 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
}
2020-03-26 21:07:15 +00:00
func ( c * CRDFinalizer ) enqueue ( obj * apiextensionsv1 . CustomResourceDefinition ) {
2019-01-12 04:58:27 +00:00
key , err := cache . DeletionHandlingMetaNamespaceKeyFunc ( obj )
if err != nil {
2019-09-27 21:51:53 +00:00
utilruntime . HandleError ( fmt . Errorf ( "couldn't get key for object %#v: %v" , obj , err ) )
2019-01-12 04:58:27 +00:00
return
}
c . queue . Add ( key )
}
func ( c * CRDFinalizer ) addCustomResourceDefinition ( obj interface { } ) {
2020-03-26 21:07:15 +00:00
castObj := obj . ( * apiextensionsv1 . CustomResourceDefinition )
2019-01-12 04:58:27 +00:00
// only queue deleted things
2020-03-26 21:07:15 +00:00
if ! castObj . DeletionTimestamp . IsZero ( ) && apiextensionshelpers . CRDHasFinalizer ( castObj , apiextensionsv1 . CustomResourceCleanupFinalizer ) {
2019-01-12 04:58:27 +00:00
c . enqueue ( castObj )
}
}
func ( c * CRDFinalizer ) updateCustomResourceDefinition ( oldObj , newObj interface { } ) {
2020-03-26 21:07:15 +00:00
oldCRD := oldObj . ( * apiextensionsv1 . CustomResourceDefinition )
newCRD := newObj . ( * apiextensionsv1 . CustomResourceDefinition )
2019-01-12 04:58:27 +00:00
// only queue deleted things that haven't been finalized by us
2020-03-26 21:07:15 +00:00
if newCRD . DeletionTimestamp . IsZero ( ) || ! apiextensionshelpers . CRDHasFinalizer ( newCRD , apiextensionsv1 . CustomResourceCleanupFinalizer ) {
2019-01-12 04:58:27 +00:00
return
}
// always requeue resyncs just in case
if oldCRD . ResourceVersion == newCRD . ResourceVersion {
c . enqueue ( newCRD )
return
}
// If the only difference is in the terminating condition, then there's no reason to requeue here. This controller
// is likely to be the originator, so requeuing would hot-loop us. Failures are requeued by the workqueue directly.
// This is a low traffic and scale resource, so the copy is terrible. It's not good, so better ideas
// are welcome.
oldCopy := oldCRD . DeepCopy ( )
newCopy := newCRD . DeepCopy ( )
oldCopy . ResourceVersion = ""
newCopy . ResourceVersion = ""
2020-03-26 21:07:15 +00:00
apiextensionshelpers . RemoveCRDCondition ( oldCopy , apiextensionsv1 . Terminating )
apiextensionshelpers . RemoveCRDCondition ( newCopy , apiextensionsv1 . Terminating )
2019-01-12 04:58:27 +00:00
if ! reflect . DeepEqual ( oldCopy , newCopy ) {
c . enqueue ( newCRD )
}
}