...

Source file src/github.com/GoogleCloudPlatform/k8s-config-connector/pkg/controller/deletiondefender/controller.go

Documentation: github.com/GoogleCloudPlatform/k8s-config-connector/pkg/controller/deletiondefender

     1  // Copyright 2022 Google LLC
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //      http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package deletiondefender
    16  
    17  import (
    18  	"context"
    19  	"fmt"
    20  	"strings"
    21  
    22  	"github.com/GoogleCloudPlatform/k8s-config-connector/pkg/k8s"
    23  
    24  	"github.com/go-logr/logr"
    25  	apiextensions "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
    26  	"k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset"
    27  	"k8s.io/apimachinery/pkg/api/errors"
    28  	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    29  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    30  	"k8s.io/apimachinery/pkg/runtime/schema"
    31  	"sigs.k8s.io/controller-runtime/pkg/builder"
    32  	"sigs.k8s.io/controller-runtime/pkg/client"
    33  	"sigs.k8s.io/controller-runtime/pkg/controller"
    34  	klog "sigs.k8s.io/controller-runtime/pkg/log"
    35  	"sigs.k8s.io/controller-runtime/pkg/manager"
    36  	"sigs.k8s.io/controller-runtime/pkg/reconcile"
    37  )
    38  
    39  var logger = klog.Log
    40  
    41  type Reconciler struct {
    42  	client.Client
    43  	// a core k8s client set, this is used for CRD GET requests. This client is used because it will not have caches with
    44  	// any configuration.
    45  	clientSet *clientset.Clientset
    46  	mgr       manager.Manager
    47  	crd       *apiextensions.CustomResourceDefinition
    48  	gvk       schema.GroupVersionKind
    49  	logger    logr.Logger
    50  }
    51  
    52  func Add(mgr manager.Manager, crd *apiextensions.CustomResourceDefinition) error {
    53  	kind := crd.Spec.Names.Kind
    54  	apiVersion := k8s.GetAPIVersionFromCRD(crd)
    55  	controllerName := fmt.Sprintf("%v-deletion-defender-controller", strings.ToLower(kind))
    56  	r, err := NewReconciler(mgr, crd)
    57  	if err != nil {
    58  		return err
    59  	}
    60  	obj := &unstructured.Unstructured{
    61  		Object: map[string]interface{}{
    62  			"kind":       kind,
    63  			"apiVersion": apiVersion,
    64  		},
    65  	}
    66  	_, err = builder.
    67  		ControllerManagedBy(mgr).
    68  		Named(controllerName).
    69  		WithOptions(controller.Options{MaxConcurrentReconciles: k8s.ControllerMaxConcurrentReconciles}).
    70  		For(obj, builder.OnlyMetadata).
    71  		Build(r)
    72  	if err != nil {
    73  		return fmt.Errorf("error creating new controller: %v", err)
    74  	}
    75  	logger.Info("Registered controller", "kind", kind, "apiVersion", apiVersion)
    76  	return nil
    77  }
    78  
    79  func NewReconciler(mgr manager.Manager, crd *apiextensions.CustomResourceDefinition) (*Reconciler, error) {
    80  	controllerName := fmt.Sprintf("%v-deletion-defender-controller", strings.ToLower(crd.Spec.Names.Kind))
    81  	clientSet, err := clientset.NewForConfig(mgr.GetConfig())
    82  	if err != nil {
    83  		return nil, fmt.Errorf("error creating new clientset: %v", err)
    84  	}
    85  	return &Reconciler{
    86  		Client:    mgr.GetClient(),
    87  		clientSet: clientSet,
    88  		mgr:       mgr,
    89  		crd:       crd,
    90  		gvk: schema.GroupVersionKind{
    91  			Group:   crd.Spec.Group,
    92  			Version: k8s.GetVersionFromCRD(crd),
    93  			Kind:    crd.Spec.Names.Kind,
    94  		},
    95  		logger: logger.WithName(controllerName),
    96  	}, nil
    97  }
    98  
    99  func (r *Reconciler) Reconcile(ctx context.Context, req reconcile.Request) (res reconcile.Result, err error) {
   100  	u := &unstructured.Unstructured{}
   101  	u.SetGroupVersionKind(r.gvk)
   102  
   103  	if err := r.Get(ctx, req.NamespacedName, u); err != nil {
   104  		if errors.IsNotFound(err) {
   105  			r.logger.Info("resource not found in API server; finishing reconcile", "resource", req.NamespacedName)
   106  			return reconcile.Result{}, nil
   107  		}
   108  		return reconcile.Result{}, err
   109  	}
   110  	if u.GetDeletionTimestamp().IsZero() || !k8s.HasFinalizer(u, k8s.DeletionDefenderFinalizerName) {
   111  		return reconcile.Result{}, nil
   112  	}
   113  
   114  	// The resource is being deleted, and has the deletion defender finalizer. Determine whether
   115  	// this resource deletion should result in a delete request to the underlying API.
   116  	r.logger.Info("starting deletion defender finalization", "resource", req.NamespacedName)
   117  
   118  	uninstalling, err := r.isUninstalling(ctx)
   119  	if err != nil {
   120  		return reconcile.Result{}, fmt.Errorf("error determining if CRD is uninstalling: %w", err)
   121  	}
   122  
   123  	// If we are uninstalling, remove both KCC finalizers and set the resource to abandon. Otherwise,
   124  	// remove just the deletion defender finalizer and allow the controller to delete the underlying
   125  	// resource on GCP.
   126  	k8s.RemoveFinalizer(u, k8s.DeletionDefenderFinalizerName)
   127  	if uninstalling {
   128  		r.logger.Info("resource type is being uninstalled; abandoning by default", "resource", req.NamespacedName)
   129  		k8s.RemoveFinalizer(u, k8s.ControllerFinalizerName)
   130  		k8s.SetAnnotation(k8s.DeletionPolicyAnnotation, k8s.DeletionPolicyAbandon, u)
   131  	}
   132  	if err := r.Update(ctx, u); err != nil {
   133  		if errors.IsConflict(err) {
   134  			return reconcile.Result{}, fmt.Errorf("couldn't update the api server due to conflict. Re-enqueue the request for another reconciliation attempt: %v", err)
   135  		}
   136  		return reconcile.Result{}, fmt.Errorf("error with update call to API server: %v", err)
   137  	}
   138  
   139  	r.logger.Info("successfully finalized deletion defense", "resource", req.NamespacedName)
   140  	return reconcile.Result{}, nil
   141  }
   142  
   143  func (r *Reconciler) isUninstalling(ctx context.Context) (bool, error) {
   144  	// Check if the associated CRD has its deletion timestamp set.
   145  	// it is important to use the clientset.Clientset here rather than the controller-runtime client.Client, because
   146  	// controller-runtime's client can have caches enabled, disabling them is tricky, and it would be easy for a bug to
   147  	// be introduced that re-enables the cache. We want the latest state of the CRD here so use the basic clientset.
   148  	crd, err := r.clientSet.ApiextensionsV1().CustomResourceDefinitions().Get(ctx, r.crd.GetName(), v1.GetOptions{})
   149  	if err != nil {
   150  		return false, fmt.Errorf("error getting CRD '%v': %w", r.crd.GetName(), err)
   151  	}
   152  	return !crd.GetDeletionTimestamp().IsZero(), nil
   153  }
   154  

View as plain text