1
2
3
4
5
6
7
8
9
10
11
12
13
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
44
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
115
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
124
125
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
145
146
147
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