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  }