github.com/openebs/node-disk-manager@v1.9.1-0.20230225014141-4531f06ffa1e/pkg/controllers/blockdeviceclaim/blockdeviceclaim_controller.go (about)

     1  /*
     2  Copyright 2021.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package blockdeviceclaim
    18  
    19  import (
    20  
    21  	"context"
    22  	"fmt"
    23  
    24  	util2 "github.com/openebs/node-disk-manager/pkg/controllers/util"
    25  
    26  	"github.com/go-logr/logr"
    27  	corev1 "k8s.io/api/core/v1"
    28  	"k8s.io/apimachinery/pkg/api/errors"
    29  	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    30  	"k8s.io/apimachinery/pkg/runtime"
    31  	"k8s.io/client-go/tools/record"
    32  	"k8s.io/client-go/tools/reference"
    33  	"k8s.io/klog/v2"
    34  	ctrl "sigs.k8s.io/controller-runtime"
    35  	"sigs.k8s.io/controller-runtime/pkg/client"
    36  	"sigs.k8s.io/controller-runtime/pkg/reconcile"
    37  
    38  	apis "github.com/openebs/node-disk-manager/api/v1alpha1"
    39  	ndm "github.com/openebs/node-disk-manager/cmd/ndm_daemonset/controller"
    40  	"github.com/openebs/node-disk-manager/db/kubernetes"
    41  	"github.com/openebs/node-disk-manager/pkg/select/blockdevice"
    42  	"github.com/openebs/node-disk-manager/pkg/select/verify"
    43  	"github.com/openebs/node-disk-manager/pkg/util"
    44  )
    45  
    46  // BlockDeviceClaimReconciler reconciles a BlockDeviceClaim object
    47  type BlockDeviceClaimReconciler struct {
    48  	Client   client.Client
    49  	Log      logr.Logger
    50  	Scheme   *runtime.Scheme
    51  	Recorder record.EventRecorder
    52  }
    53  
    54  //+kubebuilder:rbac:groups=openebs.io,resources=blockdeviceclaims,verbs=get;list;watch;create;update;patch;delete
    55  //+kubebuilder:rbac:groups=openebs.io,resources=blockdeviceclaims/status,verbs=get;update;patch
    56  //+kubebuilder:rbac:groups=openebs.io,resources=blockdeviceclaims/finalizers,verbs=update
    57  
    58  // Reconcile is part of the main kubernetes reconciliation loop which aims to
    59  // move the current state of the cluster closer to the desired state.
    60  // For more details, check Reconcile and its Result here:
    61  // - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.7.2/pkg/reconcile
    62  func (r *BlockDeviceClaimReconciler) Reconcile(ctx context.Context, request ctrl.Request) (ctrl.Result, error) {
    63  	_ = context.Background()
    64  	// _ = r.Log.WithValues("blockdeviceclaim", request.NamespacedName)
    65  
    66  	// your logic here
    67  	// Fetch the BlockDeviceClaim instance
    68  
    69  	instance := &apis.BlockDeviceClaim{}
    70  	err := r.Client.Get(context.TODO(), request.NamespacedName, instance)
    71  	if err != nil {
    72  		if errors.IsNotFound(err) {
    73  			// Request object not found, could have been deleted after reconcile request.
    74  			// Owned objects are automatically garbage collected. For additional cleanup logic use finalizers.
    75  			// Return and don't requeue
    76  			return reconcile.Result{}, nil
    77  		}
    78  		// Error reading the object - requeue the request.
    79  		return reconcile.Result{}, err
    80  	}
    81  
    82  	// check if reconciliation is disabled for this resource
    83  	if IsReconcileDisabled(instance) {
    84  		return reconcile.Result{}, nil
    85  	}
    86  
    87  	switch instance.Status.Phase {
    88  	case apis.BlockDeviceClaimStatusPending:
    89  		fallthrough
    90  	case apis.BlockDeviceClaimStatusEmpty:
    91  		klog.Infof("BDC %s claim phase is: %s", instance.Name, instance.Status.Phase)
    92  		// claim the BD only if deletion time stamp is not set.
    93  		// since BDC can now have multiple finalizers, we should not claim a
    94  		// BD if its deletiontime stamp is set.
    95  		if instance.DeletionTimestamp.IsZero() {
    96  			err := r.claimDeviceForBlockDeviceClaim(instance)
    97  			if err != nil {
    98  				klog.Errorf("%s failed to claim: %v", instance.Name, err)
    99  				return reconcile.Result{}, err
   100  			}
   101  		}
   102  	case apis.BlockDeviceClaimStatusInvalidCapacity:
   103  		// migrating state to Pending if in InvalidCapacity state.
   104  		// The InvalidCapacityState is deprecated and pending will be used.
   105  		// InvalidCapacity will be the reason for why the BDC is in Pending state.
   106  		instance.Status.Phase = apis.BlockDeviceClaimStatusPending
   107  		err := r.updateClaimStatus(apis.BlockDeviceClaimStatusPending, instance)
   108  		if err != nil {
   109  			klog.Errorf("error in updating phase to pending from invalid capacity for %s: %v", instance.Name, err)
   110  		}
   111  		klog.Infof("%s claim phase is: %s", instance.Name, instance.Status.Phase)
   112  	case apis.BlockDeviceClaimStatusDone:
   113  		err := r.FinalizerHandling(instance)
   114  		if err != nil {
   115  			klog.Errorf("Finalizer handling failed for %s: %v", instance.Name, err)
   116  			return reconcile.Result{}, err
   117  		}
   118  	}
   119  
   120  	return ctrl.Result{}, nil
   121  }
   122  
   123  // claimDeviceForBlockDeviceClaim is created, try to determine blockdevice which is
   124  // free and has size equal/greater than BlockDeviceClaim request.
   125  func (r *BlockDeviceClaimReconciler) claimDeviceForBlockDeviceClaim(instance *apis.BlockDeviceClaim) error {
   126  
   127  	config := blockdevice.NewConfig(&instance.Spec, r.Client)
   128  
   129  	// check for capacity only in auto selection
   130  	if !config.ManualSelection {
   131  		// perform verification of the claim, like capacity
   132  		// Get the capacity requested in the claim
   133  		_, err := verify.GetRequestedCapacity(instance.Spec.Resources.Requests)
   134  		if err != nil {
   135  			r.Recorder.Eventf(instance, corev1.EventTypeWarning, "InvalidCapacity", "Invalid Capacity requested")
   136  			//Update deviceClaim CR with pending status
   137  			instance.Status.Phase = apis.BlockDeviceClaimStatusPending
   138  			err1 := r.updateClaimStatus(instance.Status.Phase, instance)
   139  			if err1 != nil {
   140  				klog.Errorf("%s requested an invalid capacity: %v", instance.Name, err1)
   141  				return err1
   142  			}
   143  			klog.Infof("%s set to Pending due to invalid capacity request", instance.Name)
   144  			return err
   145  		}
   146  	}
   147  
   148  	// create selector from the label selector given in BDC spec.
   149  	selector := generateSelector(*instance)
   150  
   151  	// get list of block devices.
   152  	bdList, err := r.getListofDevices(selector)
   153  	if err != nil {
   154  		return err
   155  	}
   156  
   157  	selectedDevice, err := config.Filter(bdList)
   158  	if err != nil {
   159  		klog.Errorf("Error selecting device for %s: %v", instance.Name, err)
   160  		r.Recorder.Eventf(instance, corev1.EventTypeWarning, "SelectionFailed", err.Error())
   161  		instance.Status.Phase = apis.BlockDeviceClaimStatusPending
   162  	} else {
   163  		instance.Spec.BlockDeviceName = selectedDevice.Name
   164  		instance.Status.Phase = apis.BlockDeviceClaimStatusDone
   165  		err = r.claimBlockDevice(selectedDevice, instance)
   166  		if err != nil {
   167  			return err
   168  		}
   169  		r.Recorder.Eventf(selectedDevice, corev1.EventTypeNormal, "BlockDeviceClaimed", "BlockDevice claimed by %v", instance.Name)
   170  		r.Recorder.Eventf(instance, corev1.EventTypeNormal, "BlockDeviceClaimed", "BlockDevice: %v claimed", instance.Spec.BlockDeviceName)
   171  	}
   172  
   173  	return r.updateClaimStatus(instance.Status.Phase, instance)
   174  }
   175  
   176  // FinalizerHandling removes the finalizer from the claim resource
   177  func (r *BlockDeviceClaimReconciler) FinalizerHandling(instance *apis.BlockDeviceClaim) error {
   178  
   179  	if instance.ObjectMeta.DeletionTimestamp.IsZero() {
   180  		return nil
   181  	}
   182  	// The object is being deleted
   183  	// Check if the BDC has only NDM finalizer present. If yes, it means that the BDC
   184  	// was deleted by the owner itself, and NDM can proceed with releasing the BD and
   185  	// removing the NDM finalizer
   186  	if len(instance.ObjectMeta.Finalizers) == 1 &&
   187  		util.Contains(instance.ObjectMeta.Finalizers, util2.BlockDeviceClaimFinalizer) {
   188  		// Finalizer is set, lets handle external dependency
   189  		if err := r.releaseClaimedBlockDevice(instance); err != nil {
   190  			klog.Errorf("Error releasing claimed block device %s from %s: %v",
   191  				instance.Spec.BlockDeviceName, instance.Name, err)
   192  			return err
   193  		}
   194  		r.Recorder.Eventf(instance, corev1.EventTypeNormal, "BlockDeviceReleased", "BlockDevice: %v is released", instance.Spec.BlockDeviceName)
   195  
   196  		// Remove finalizer from list and update it.
   197  		instance.ObjectMeta.Finalizers = util.RemoveString(instance.ObjectMeta.Finalizers, util2.BlockDeviceClaimFinalizer)
   198  		if err := r.Client.Update(context.TODO(), instance); err != nil {
   199  			klog.Errorf("Error removing finalizer from %s", instance.Name)
   200  			r.Recorder.Eventf(instance, corev1.EventTypeWarning, "UpdateOperationFailed", "Unable to remove Finalizer, due to error: %v", err.Error())
   201  			return err
   202  		}
   203  	}
   204  
   205  	return nil
   206  }
   207  
   208  func (r *BlockDeviceClaimReconciler) updateClaimStatus(phase apis.DeviceClaimPhase,
   209  	instance *apis.BlockDeviceClaim) error {
   210  	switch phase {
   211  	case apis.BlockDeviceClaimStatusDone:
   212  		instance.ObjectMeta.Finalizers = append(instance.ObjectMeta.Finalizers, util2.BlockDeviceClaimFinalizer)
   213  		r.Recorder.Eventf(instance, corev1.EventTypeNormal, "BlockDeviceClaimBound", "BlockDeviceClaim is bound to %v", instance.Spec.BlockDeviceName)
   214  
   215  	}
   216  	// Update BlockDeviceClaim CR
   217  	err := r.Client.Update(context.TODO(), instance)
   218  	if err != nil {
   219  		return fmt.Errorf("error updating status of BDC : %s, %v", instance.ObjectMeta.Name, err)
   220  	}
   221  
   222  	return nil
   223  }
   224  
   225  // isDeviceRequestedByThisDeviceClaim checks whether a claimed block device belongs to the given BDC
   226  func (r *BlockDeviceClaimReconciler) isDeviceRequestedByThisDeviceClaim(
   227  	instance *apis.BlockDeviceClaim, item apis.BlockDevice) bool {
   228  
   229  	if item.Status.ClaimState != apis.BlockDeviceClaimed {
   230  		return false
   231  
   232  	}
   233  
   234  	if item.Spec.ClaimRef.Name != instance.ObjectMeta.Name {
   235  		return false
   236  	}
   237  
   238  	if item.Spec.ClaimRef.UID != instance.ObjectMeta.UID {
   239  		return false
   240  	}
   241  
   242  	if item.Spec.ClaimRef.Kind != instance.TypeMeta.Kind {
   243  		return false
   244  	}
   245  	return true
   246  }
   247  
   248  // releaseClaimedBlockDevice releases the block device claimed by this BlockDeviceClaim
   249  func (r *BlockDeviceClaimReconciler) releaseClaimedBlockDevice(
   250  	instance *apis.BlockDeviceClaim) error {
   251  
   252  	klog.Infof("Releasing claimed block device %s from %s", instance.Spec.BlockDeviceName, instance.Name)
   253  
   254  	//Get BlockDevice list on all nodes
   255  	//empty selector is used to select everything.
   256  	selector := &v1.LabelSelector{}
   257  	bdList, err := r.getListofDevices(selector)
   258  	if err != nil {
   259  		return err
   260  	}
   261  
   262  	// Check if same deviceclaim holding the ObjRef
   263  	var claimedBd *apis.BlockDevice
   264  	for i := range bdList.Items {
   265  		// Found a blockdevice ObjRef with BlockDeviceClaim, Clear
   266  		// ObjRef and mark blockdevice released in etcd
   267  		if r.isDeviceRequestedByThisDeviceClaim(instance, bdList.Items[i]) {
   268  			claimedBd = &bdList.Items[i]
   269  			break
   270  		}
   271  	}
   272  	// This case occurs when a claimed BD is manually deleted by removing the finalizer.
   273  	// If this check is not performed, the NDM operator will continuously crash, because it
   274  	// will try to release a non existent BD.
   275  	if claimedBd == nil {
   276  		r.Recorder.Eventf(instance, corev1.EventTypeWarning, "BlockDeviceNotFound", "BlockDevice %s not found for releasing", instance.Spec.BlockDeviceName)
   277  		klog.Errorf("could not find blockdevice for claim: %s", instance.Name)
   278  		return fmt.Errorf("blockdevice: %s not found for releasing from bdc: %s", instance.Spec.BlockDeviceName, instance.Name)
   279  	}
   280  
   281  	dvr := claimedBd.DeepCopy()
   282  	dvr.Spec.ClaimRef = nil
   283  	dvr.Status.ClaimState = apis.BlockDeviceReleased
   284  
   285  	err = r.Client.Update(context.TODO(), dvr)
   286  	if err != nil {
   287  		klog.Errorf("Error updating ClaimRef of %s: %v", dvr.Name, err)
   288  		return err
   289  	}
   290  	r.Recorder.Eventf(dvr, corev1.EventTypeNormal, "BlockDeviceCleanUpInProgress", "Released from BDC: %v", instance.Name)
   291  
   292  	return nil
   293  }
   294  
   295  // claimBlockDevice is used to claim the passed on blockdevice
   296  func (r *BlockDeviceClaimReconciler) claimBlockDevice(bd *apis.BlockDevice, instance *apis.BlockDeviceClaim) error {
   297  	claimRef, err := reference.GetReference(r.Scheme, instance)
   298  	if err != nil {
   299  		return fmt.Errorf("error getting claim reference for BDC:%s, %v", instance.ObjectMeta.Name, err)
   300  	}
   301  	// add finalizer to BlockDevice to prevent accidental deletion of BD
   302  	bd.Finalizers = append(bd.Finalizers, util2.BlockDeviceFinalizer)
   303  	bd.Spec.ClaimRef = claimRef
   304  	bd.Status.ClaimState = apis.BlockDeviceClaimed
   305  	err = r.Client.Update(context.TODO(), bd)
   306  	if err != nil {
   307  		return fmt.Errorf("error while updating BD:%s, %v", bd.ObjectMeta.Name, err)
   308  	}
   309  	klog.Infof("%s claimed by %s", bd.Name, instance.Name)
   310  	return nil
   311  }
   312  
   313  // GetBlockDevice get block device resource from etcd
   314  func (r *BlockDeviceClaimReconciler) GetBlockDevice(name string) (*apis.BlockDevice, error) {
   315  	bd := &apis.BlockDevice{}
   316  	err := r.Client.Get(context.TODO(),
   317  		client.ObjectKey{Namespace: "", Name: name}, bd)
   318  
   319  	if err != nil {
   320  		return nil, err
   321  	}
   322  	return bd, nil
   323  }
   324  
   325  // getListofDevices gets the list of block devices on the node to which BlockDeviceClaim is made
   326  // TODO:
   327  //  ListBlockDeviceResource in package cmd/ndm_daemonset/controller has the same functionality.
   328  //  Need to merge these 2 functions.
   329  func (r *BlockDeviceClaimReconciler) getListofDevices(selector *v1.LabelSelector) (*apis.BlockDeviceList, error) {
   330  
   331  	//Initialize a deviceList object.
   332  	listBlockDevice := &apis.BlockDeviceList{
   333  		TypeMeta: v1.TypeMeta{
   334  			Kind:       apis.BlockDeviceResourceKind,
   335  			APIVersion: apis.GroupVersion.Version,
   336  		},
   337  	}
   338  
   339  	opts := &client.ListOptions{}
   340  
   341  	if sel, err := v1.LabelSelectorAsSelector(selector); err != nil {
   342  		// if conversion of selector errors out, the list call will be errored
   343  		return nil, err
   344  	} else {
   345  		opts.LabelSelector = sel
   346  	}
   347  
   348  	//Fetch deviceList with matching criteria
   349  	err := r.Client.List(context.TODO(), listBlockDevice, opts)
   350  	if err != nil {
   351  		return nil, err
   352  	}
   353  
   354  	return listBlockDevice, nil
   355  }
   356  
   357  // IsReconcileDisabled is used to check if reconciliation is disabled for
   358  // BlockDeviceClaim
   359  func IsReconcileDisabled(bdc *apis.BlockDeviceClaim) bool {
   360  	return bdc.Annotations[ndm.OpenEBSReconcile] == "false"
   361  }
   362  
   363  // generateSelector creates the label selector for BlockDevices from
   364  // the BlockDeviceClaim spec
   365  func generateSelector(bdc apis.BlockDeviceClaim) *v1.LabelSelector {
   366  	var hostName string
   367  	// get the hostname
   368  	if len(bdc.Spec.HostName) != 0 {
   369  		hostName = bdc.Spec.HostName
   370  	}
   371  	// the hostname in NodeAttribute will override the hostname in spec, since spec.hostName
   372  	// will be deprecated shortly
   373  	if len(bdc.Spec.BlockDeviceNodeAttributes.HostName) != 0 {
   374  		hostName = bdc.Spec.BlockDeviceNodeAttributes.HostName
   375  	}
   376  
   377  	// the hostname label is added into the user given list of labels. If the user hasn't
   378  	// given any selector, then the selector object is initialized.
   379  	selector := bdc.Spec.Selector.DeepCopy()
   380  	if selector == nil {
   381  		selector = &v1.LabelSelector{}
   382  	}
   383  	if selector.MatchLabels == nil {
   384  		selector.MatchLabels = make(map[string]string)
   385  	}
   386  
   387  	// if any hostname is provided, add it to selector
   388  	if len(hostName) != 0 {
   389  		selector.MatchLabels[kubernetes.KubernetesHostNameLabel] = hostName
   390  	}
   391  	return selector
   392  }
   393  
   394  func (r *BlockDeviceClaimReconciler) SetupWithManager(mgr ctrl.Manager) error {
   395  
   396  	return ctrl.NewControllerManagedBy(mgr).
   397  		For(&apis.BlockDeviceClaim{}).
   398  		Owns(&apis.BlockDeviceClaim{}).
   399  		Complete(r)
   400  }