github.com/banzaicloud/operator-tools@v0.28.10/pkg/inventory/inventory.go (about) 1 // Copyright © 2020 Banzai Cloud 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 inventory 16 17 import ( 18 "context" 19 "fmt" 20 "strings" 21 22 "emperror.dev/errors" 23 "github.com/go-logr/logr" 24 core "k8s.io/api/core/v1" 25 apierrors "k8s.io/apimachinery/pkg/api/errors" 26 "k8s.io/apimachinery/pkg/api/meta" 27 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 28 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 29 "k8s.io/apimachinery/pkg/runtime" 30 "k8s.io/apimachinery/pkg/runtime/schema" 31 "k8s.io/apimachinery/pkg/types" 32 "k8s.io/client-go/discovery" 33 "sigs.k8s.io/controller-runtime/pkg/client" 34 35 "github.com/banzaicloud/operator-tools/pkg/reconciler" 36 "github.com/banzaicloud/operator-tools/pkg/utils" 37 ) 38 39 const ( 40 CustomResourceDefinition = "CustomResourceDefinition" 41 Namespace = "Namespace" 42 43 referencesKey = "refs" 44 ) 45 46 // State holder between reconcile phases 47 type InventoryData struct { 48 ObjectsToDelete []runtime.Object 49 CurrentObjects []runtime.Object 50 DesiredObjects []runtime.Object 51 } 52 53 // A generalized structure to enable us to attach additional inventory management 54 // around the native reconcile loop and adds the capability to store state between 55 // reconcile phases (see operator-tool's NativeReconciler) 56 type Inventory struct { 57 genericClient client.Client 58 log logr.Logger 59 inventoryData InventoryData 60 61 // map of GVK of cluster scoped API resources 62 clusterScopedAPIResources map[string]struct{} 63 64 // Discovery client to look up API resources 65 discoveryClient discovery.DiscoveryInterface 66 } 67 68 func NewInventory(client client.Client, log logr.Logger, clusterResources map[string]struct{}) (*Inventory, error) { 69 if clusterResources == nil { 70 return nil, errors.New("list of cluster scoped resources is required") 71 } 72 return &Inventory{ 73 genericClient: client, 74 log: log, 75 clusterScopedAPIResources: clusterResources, 76 }, nil 77 } 78 79 func NewDiscoveryInventory(client client.Client, log logr.Logger, discovery discovery.DiscoveryInterface) *Inventory { 80 return &Inventory{ 81 genericClient: client, 82 log: log, 83 discoveryClient: discovery, 84 } 85 } 86 87 func CreateObjectsInventory(namespace, name string, objects []runtime.Object) (*core.ConfigMap, error) { 88 resourceURLs := make([]string, len(objects)) 89 for i := range objects { 90 objMeta, err := meta.Accessor(objects[i]) 91 if err != nil { 92 return nil, err 93 } 94 objGVK := objects[i].GetObjectKind().GroupVersionKind() 95 resourceURLs[i] = fmt.Sprintf("%s/%s/%s/%s/%s", 96 objGVK.Group, 97 objGVK.Version, 98 objGVK.Kind, 99 objMeta.GetNamespace(), 100 objMeta.GetName()) 101 } 102 cm := core.ConfigMap{ 103 TypeMeta: metav1.TypeMeta{ 104 Kind: "ConfigMap", 105 APIVersion: "v1", 106 }, 107 ObjectMeta: metav1.ObjectMeta{ 108 Namespace: namespace, 109 Name: name, 110 }, 111 Immutable: utils.BoolPointer(false), 112 Data: map[string]string{ 113 referencesKey: strings.Join(resourceURLs, ","), 114 }, 115 } 116 117 return &cm, nil 118 } 119 120 func GetObjectsFromInventory(inventory core.ConfigMap) (objects []runtime.Object) { 121 resourceURLs := strings.Split(inventory.Data[referencesKey], ",") 122 123 for i := range resourceURLs { 124 if resourceURLs[i] == "" { 125 continue 126 } 127 parts := strings.Split(resourceURLs[i], "/") 128 129 u := &unstructured.Unstructured{} 130 u.SetGroupVersionKind(schema.GroupVersionKind{ 131 Group: parts[0], 132 Version: parts[1], 133 Kind: parts[2], 134 }) 135 u.SetNamespace(parts[3]) 136 u.SetName(parts[4]) 137 138 objects = append(objects, u) 139 } 140 141 return 142 } 143 144 // Hand over a GVK list to the native reconcile loop's purge method 145 func (c *Inventory) TypesToPurge() []schema.GroupVersionKind { 146 currentObjects := c.inventoryData.ObjectsToDelete 147 groupVersionKindDict := make(map[schema.GroupVersionKind]struct{}) 148 149 for _, currentObject := range currentObjects { 150 gvk := currentObject.GetObjectKind().GroupVersionKind() 151 152 if gvk.Kind == CustomResourceDefinition || gvk.Kind == Namespace { 153 continue 154 } 155 156 if objMeta, err := meta.Accessor(currentObject); err == nil { 157 c.log.V(1).Info("mark object for deletion", "gvk", gvk.String(), "namespace", objMeta.GetNamespace(), "name", objMeta.GetName()) 158 groupVersionKindDict[gvk] = struct{}{} 159 } 160 } 161 162 groupVersionKindList := make([]schema.GroupVersionKind, 0, len(groupVersionKindDict)) 163 for k := range groupVersionKindDict { 164 groupVersionKindList = append(groupVersionKindList, k) 165 } 166 167 return groupVersionKindList 168 } 169 170 // Fetch list of resources made by the previous reconcile loop and store into an attached context 171 // Return a new list of resources which will be reconciled among with the other resources we listed here 172 func (c *Inventory) PrepareDesiredObjects(ns, componentName string, parent reconciler.ResourceOwner, resourceBuilders []reconciler.ResourceBuilder) (*core.ConfigMap, error) { 173 var err error 174 var desiredObjects []runtime.Object 175 var objectsInventory core.ConfigMap 176 objectsInventoryName := fmt.Sprintf("%s-%s-%s-object-inventory", parent.GetName(), ns, componentName) 177 178 // collect 179 err = c.genericClient.Get(context.TODO(), types.NamespacedName{Namespace: ns, Name: objectsInventoryName}, &objectsInventory) 180 if err != nil && !meta.IsNoMatchError(err) && !apierrors.IsNotFound(err) { 181 return nil, errors.WrapIfWithDetails(err, 182 "during object inventory fetch...", 183 "namespace", ns, "component", componentName, "inventoryName", objectsInventoryName) 184 } 185 c.inventoryData.CurrentObjects = GetObjectsFromInventory(objectsInventory) 186 187 // desired 188 for _, builder := range resourceBuilders { 189 obj, state, err := builder() 190 if err != nil { 191 return nil, errors.WrapIfWithDetails(err, 192 "couldn't build desired object...", 193 "namespace", ns, "component", componentName) 194 } 195 if state != reconciler.StateAbsent { 196 desiredObjects = append(desiredObjects, obj) 197 } 198 } 199 200 // sanitize desired objects 201 err = c.sanitizeDesiredObjects(desiredObjects) 202 if err != nil { 203 return nil, errors.WrapIfWithDetails(err, 204 "couldn't sanitize desired objects", 205 "namespace", ns, "component", componentName) 206 } 207 208 // ensure namespace 209 err = c.ensureNamespace(ns, desiredObjects) 210 if err != nil { 211 return nil, errors.WrapIfWithDetails(err, 212 "couldn't ensure namespace meta field on desired objects", 213 "namespace", ns, "component", componentName) 214 } 215 216 // create inventory of created objects 217 if newInventory, err := CreateObjectsInventory(ns, objectsInventoryName, desiredObjects); err == nil { 218 c.inventoryData.DesiredObjects = desiredObjects 219 return newInventory, nil 220 } 221 222 return nil, errors.WrapIfWithDetails(err, 223 "during object inventory creation...", 224 "namespace", ns, "inventoryName", objectsInventoryName) 225 } 226 227 // Collect `missing` resources from desired state 228 func (c *Inventory) PrepareDeletableObjects() error { 229 var deleteObjects []runtime.Object 230 231 currentObjects := c.inventoryData.CurrentObjects 232 for _, currentObject := range currentObjects { 233 metaobj, err := meta.Accessor(currentObject) 234 if err != nil { 235 return errors.WrapIfWithDetails(err, 236 "could not access object metadata", 237 "gvk", currentObject.GetObjectKind().GroupVersionKind().String()) 238 } 239 240 isClusterScoped, err := c.IsClusterScoped(currentObject) 241 if err != nil { 242 c.log.Error(err, "scope check failed, unable to determine whether object is eligible for deletion") 243 continue 244 } 245 // check if current object still exists 246 if !isClusterScoped && metaobj.GetNamespace() == "" { 247 c.log.Info("object namespace is unknown, unable to determine whether is eligible for deletion", "gvk", currentObject.GetObjectKind().GroupVersionKind().String(), "name", metaobj.GetName()) 248 continue 249 } 250 err = c.genericClient.Get(context.TODO(), types.NamespacedName{Namespace: metaobj.GetNamespace(), Name: metaobj.GetName()}, currentObject.(client.Object)) 251 if err != nil && !meta.IsNoMatchError(err) && !apierrors.IsNotFound(err) { 252 return errors.WrapIfWithDetails(err, 253 "could not verify if object exists", 254 "namespace", metaobj.GetNamespace(), "objectName", metaobj.GetName()) 255 } 256 257 currentObjGVK := currentObject.GetObjectKind().GroupVersionKind() 258 259 if metaobj.GetDeletionTimestamp() != nil || currentObjGVK.Kind == CustomResourceDefinition || currentObjGVK.Kind == Namespace { 260 continue 261 } 262 263 desiredObjects := c.inventoryData.DesiredObjects 264 found := false 265 266 for _, desiredObject := range desiredObjects { 267 desiredObjGVK := desiredObject.GetObjectKind().GroupVersionKind() 268 desiredObjMeta, _ := meta.Accessor(desiredObject) 269 270 if currentObjGVK.Group == desiredObjGVK.Group && 271 currentObjGVK.Version == desiredObjGVK.Version && 272 currentObjGVK.Kind == desiredObjGVK.Kind && 273 metaobj.GetNamespace() == desiredObjMeta.GetNamespace() && 274 metaobj.GetName() == desiredObjMeta.GetName() { 275 found = true 276 break 277 } 278 } 279 if !found { 280 c.log.Info("object eligible for delete", "gvk", currentObjGVK.String(), "namespace", metaobj.GetNamespace(), "name", metaobj.GetName()) 281 deleteObjects = append(deleteObjects, currentObject) 282 } 283 } 284 285 c.inventoryData.ObjectsToDelete = deleteObjects 286 return nil 287 } 288 289 // sanitizeDesiredObjects cleans up the passed desired objects 290 func (c *Inventory) sanitizeDesiredObjects(desiredObjects []runtime.Object) error { 291 for i := range desiredObjects { 292 objMeta, err := meta.Accessor(desiredObjects[i]) 293 if err != nil { 294 return errors.WrapIfWithDetails(err, "couldn't get meta data access for object", "gvk", desiredObjects[i].GetObjectKind().GroupVersionKind().String()) 295 } 296 297 isClusterScoped, err := c.IsClusterScoped(desiredObjects[i]) 298 if err != nil { 299 c.log.Error(err, "scope check failed") 300 continue 301 } 302 303 if isClusterScoped && objMeta.GetNamespace() != "" { 304 c.log.V(2).Info("removing namespace field from cluster scoped object", "gvk", desiredObjects[i].GetObjectKind().GroupVersionKind().String(), "name", objMeta.GetName()) 305 objMeta.SetNamespace("") 306 } 307 } 308 return nil 309 } 310 311 // IsClusterScoped returns true of the type if the specified resource is of cluster scope. 312 // It returns false for namespace scoped resources. 313 func (c *Inventory) IsClusterScoped(obj runtime.Object) (bool, error) { 314 gv, k := obj.GetObjectKind().GroupVersionKind().ToAPIVersionAndKind() 315 gvk := strings.Join([]string{gv, k}, "/") 316 317 if c.clusterScopedAPIResources != nil { 318 _, ok := c.clusterScopedAPIResources[gvk] 319 return ok, nil 320 } 321 322 actualGK := obj.GetObjectKind().GroupVersionKind().GroupKind() 323 324 if namespaced, ok := getStaticResourceScope(actualGK); ok { 325 return !namespaced, nil 326 } 327 328 var fresh bool 329 var err error 330 331 fresh, err = initializeAPIResources(c.discoveryClient) 332 if err != nil { 333 return false, err 334 } 335 336 if namespaced, ok := getDynamicResourceScope(actualGK); ok { 337 return !namespaced, nil 338 } 339 340 if !fresh { 341 c.log.Info("API resource not found for object in the cache, updating resource list", "gk", actualGK.String()) 342 if err := discoverAPIResources(c.discoveryClient); err != nil { 343 return false, err 344 } 345 } 346 347 if namespaced, ok := getDynamicResourceScope(actualGK); ok { 348 return !namespaced, nil 349 } 350 351 return false, errors.Errorf("unknown resource %s", actualGK.String()) 352 } 353 354 // ensureNamespace sets `namespace` as namespace for namespace scoped objects that have no namespace set 355 func (c *Inventory) ensureNamespace(namespace string, objects []runtime.Object) error { 356 for i := range objects { 357 objMeta, err := meta.Accessor(objects[i]) 358 if err != nil { 359 return errors.WrapIfWithDetails(err, "couldn't get meta data access for object", "gvk", objects[i].GetObjectKind().GroupVersionKind().String()) 360 } 361 362 isClusterScoped, err := c.IsClusterScoped(objects[i]) 363 if err != nil { 364 c.log.Error(err, "scope check failed") 365 continue 366 } 367 368 if !isClusterScoped && objMeta.GetNamespace() == "" { 369 c.log.V(2).Info("setting namespace field for namespace scoped object", "gvk", objects[i].GetObjectKind().GroupVersionKind().String(), "name", objMeta.GetName()) 370 objMeta.SetNamespace(namespace) 371 } 372 } 373 return nil 374 } 375 376 func (i *Inventory) Append(namespace, component string, parent reconciler.ResourceOwner, resourceBuilders []reconciler.ResourceBuilder) ([]reconciler.ResourceBuilder, error) { 377 ns := &core.Namespace{} 378 // get the namespace so that we can see if it's under deletion 379 // we don't care if the namespace does not exist, we might be preparing to run this for the first time 380 if err := i.genericClient.Get(context.TODO(), client.ObjectKey{Name: namespace}, ns); client.IgnoreNotFound(err) != nil { 381 return resourceBuilders, err 382 } 383 if objectInventory, err := i.PrepareDesiredObjects(namespace, component, parent, resourceBuilders); err == nil { 384 if err := i.PrepareDeletableObjects(); err != nil { 385 return resourceBuilders, err 386 } 387 // do not try to create the inventory when the namespace is being deleted 388 // or the parent resource is being deleted 389 // or the objects references are empty 390 if ns.GetDeletionTimestamp().IsZero() && parent.GetDeletionTimestamp().IsZero() && objectInventory.Data[referencesKey] != "" { 391 resourceBuilders = append(resourceBuilders, func() (runtime.Object, reconciler.DesiredState, error) { 392 return objectInventory, reconciler.StatePresent, err 393 }) 394 } 395 } 396 return resourceBuilders, nil 397 }