github.com/verrazzano/verrazzano-monitoring-operator@v0.0.30/pkg/vmo/pvc.go (about)

     1  // Copyright (C) 2020, 2022, Oracle and/or its affiliates.
     2  // Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl.
     3  
     4  package vmo
     5  
     6  import (
     7  	"context"
     8  	"errors"
     9  	"fmt"
    10  	"sort"
    11  
    12  	vmcontrollerv1 "github.com/verrazzano/verrazzano-monitoring-operator/pkg/apis/vmcontroller/v1"
    13  	"github.com/verrazzano/verrazzano-monitoring-operator/pkg/config"
    14  	"github.com/verrazzano/verrazzano-monitoring-operator/pkg/constants"
    15  	"github.com/verrazzano/verrazzano-monitoring-operator/pkg/resources/pvcs"
    16  	corev1 "k8s.io/api/core/v1"
    17  	storagev1 "k8s.io/api/storage/v1"
    18  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    19  	"k8s.io/apimachinery/pkg/labels"
    20  	"k8s.io/apimachinery/pkg/util/runtime"
    21  )
    22  
    23  // CreatePersistentVolumeClaims Creates PVCs for the given VMO instance.  Returns a pvc->AD map, which is populated *only if* AD information
    24  // can be specified for new PVCs or determined from existing PVCs.  A pvc-AD map with empty AD values instructs the
    25  // subsequent deployment processing logic to do the job of choosing ADs.
    26  func CreatePersistentVolumeClaims(controller *Controller, vmo *vmcontrollerv1.VerrazzanoMonitoringInstance) (map[string]string, error) {
    27  	// Update storage with the new API
    28  	setPerNodeStorage(vmo)
    29  	// Inspect the Storage Class to use
    30  	storageClass, err := determineStorageClass(controller, vmo.Spec.StorageClass)
    31  	if err != nil {
    32  		return nil, err
    33  	}
    34  	storageClassInfo := parseStorageClassInfo(storageClass, controller.operatorConfig)
    35  
    36  	expectedPVCs, err := pvcs.New(vmo, storageClass.Name)
    37  	if err != nil {
    38  		controller.log.Errorf("Failed to create PVC specs for VMI %s: %v", vmo.Name, err)
    39  		return nil, err
    40  	}
    41  	pvcToAdMap := map[string]string{}
    42  
    43  	controller.log.Oncef("Creating/updating PVCs for VMI %s", vmo.Name)
    44  
    45  	// Get total list of all possible schedulable ADs
    46  	schedulableADs, err := getSchedulableADs(controller)
    47  	if err != nil {
    48  		return pvcToAdMap, err
    49  	}
    50  
    51  	elasticsearchAdCounter := NewAdPvcCounter(schedulableADs)
    52  
    53  	if len(expectedPVCs) > 0 && storageClassInfo.Name == "" {
    54  		return nil, fmt.Errorf("cannot create PVCs when the cluster has no storage class")
    55  	}
    56  	for _, expectedPVC := range expectedPVCs {
    57  		pvcName := expectedPVC.Name
    58  		if pvcName == "" {
    59  			// We choose to absorb the error here as the worker would requeue the
    60  			// resource otherwise. Instead, the next time the resource is updated
    61  			// the resource will be queued again.
    62  			runtime.HandleError(errors.New(("Failed, PVC name must be specified")))
    63  			return pvcToAdMap, nil
    64  		}
    65  
    66  		controller.log.Debugf("Applying PVC '%s' in namespace '%s' for VMI '%s'\n", pvcName, vmo.Namespace, vmo.Name)
    67  		existingPvc, err := controller.pvcLister.PersistentVolumeClaims(vmo.Namespace).Get(pvcName)
    68  
    69  		// If the PVC already exists, we check if it needs resizing
    70  		if existingPvc != nil {
    71  			if pvcNeedsResize(existingPvc, expectedPVC) {
    72  				if newPVCName, err := resizePVC(controller, vmo, existingPvc, expectedPVC, storageClass); err != nil {
    73  					return nil, err
    74  				} else if newPVCName != nil {
    75  					// we need to wait until the PVC is bound
    76  					return pvcToAdMap, nil
    77  				}
    78  			}
    79  
    80  			if storageClassInfo.PvcAcceptsZone {
    81  				zone := getZoneFromExistingPvc(storageClassInfo, existingPvc)
    82  				pvcToAdMap[pvcName] = zone
    83  				if isOpenSearchPVC(existingPvc) {
    84  					elasticsearchAdCounter.Inc(zone)
    85  				}
    86  			} else {
    87  				pvcToAdMap[pvcName] = ""
    88  			}
    89  		} else {
    90  			// If the StorageClass allows us to specify zone info on the PVC, we'll do that now
    91  			var newAd string
    92  			if storageClassInfo.PvcAcceptsZone {
    93  				if isOpenSearchPVC(expectedPVC) {
    94  					newAd = elasticsearchAdCounter.GetLeastUsedAd()
    95  					elasticsearchAdCounter.Inc(newAd)
    96  				} else {
    97  					newAd = chooseRandomElementFromSlice(schedulableADs)
    98  				}
    99  				expectedPVC.Spec.Selector = &metav1.LabelSelector{MatchLabels: map[string]string{storageClassInfo.PvcZoneMatchLabel: newAd}}
   100  			}
   101  			controller.log.Oncef("Creating PVC %s in AD %s", expectedPVC.Name, newAd)
   102  
   103  			_, err = controller.kubeclientset.CoreV1().PersistentVolumeClaims(vmo.Namespace).Create(context.TODO(), expectedPVC, metav1.CreateOptions{})
   104  
   105  			if err != nil {
   106  				return pvcToAdMap, err
   107  			}
   108  
   109  			pvcToAdMap[pvcName] = newAd
   110  
   111  		}
   112  		if err != nil {
   113  			return pvcToAdMap, err
   114  		}
   115  		controller.log.Debugf("Successfully applied PVC '%s'\n", pvcName)
   116  	}
   117  
   118  	return pvcToAdMap, cleanupUnusedPVCs(controller, vmo)
   119  }
   120  
   121  // AdPvcCounter type for AD PVC counts
   122  type AdPvcCounter struct {
   123  	pvcCountByAd map[string]int
   124  }
   125  
   126  // NewAdPvcCounter return new counter.  The provided ADs are the only ones schedulable; create entries in the map
   127  func NewAdPvcCounter(ads []string) *AdPvcCounter {
   128  	var counter AdPvcCounter
   129  	counter.pvcCountByAd = make(map[string]int)
   130  	for _, ad := range ads {
   131  		counter.pvcCountByAd[ad] = 0
   132  	}
   133  	return &counter
   134  }
   135  
   136  // Inc increments counter. Any AD not already in map is not schedulable, so ignore
   137  func (p *AdPvcCounter) Inc(ad string) {
   138  	if _, ok := p.pvcCountByAd[ad]; ok {
   139  		p.pvcCountByAd[ad] = p.pvcCountByAd[ad] + 1
   140  	}
   141  }
   142  
   143  // GetLeastUsedAd returns least used AD
   144  func (p *AdPvcCounter) GetLeastUsedAd() string {
   145  	adsByPvcCount := make(map[int][]string)
   146  	var pvcCounts []int
   147  	for ad, count := range p.pvcCountByAd {
   148  		adsByPvcCount[count] = append(adsByPvcCount[count], ad)
   149  		pvcCounts = append(pvcCounts, count)
   150  	}
   151  	if len(pvcCounts) == 0 {
   152  		return ""
   153  	}
   154  	// Now sort the PVC-counts-per-AD to put the smallest count at element 0
   155  	sort.Ints(pvcCounts)
   156  	// Get the array of ADs that have that smallest PVC count, and pick one at random
   157  	candidateAds := adsByPvcCount[pvcCounts[0]]
   158  	return chooseRandomElementFromSlice(candidateAds)
   159  }
   160  
   161  // Determines the storage class to use for the current environment
   162  func determineStorageClass(controller *Controller, className *string) (*storagev1.StorageClass, error) {
   163  	storageClass, err := getStorageClassOverride(controller, className)
   164  	if err != nil {
   165  		return nil, err
   166  	}
   167  	if storageClass != nil {
   168  		return storageClass, nil
   169  	}
   170  
   171  	// Otherwise we'll use the "default" storage class
   172  	storageClasses, err := controller.storageClassLister.List(labels.Everything())
   173  	if err != nil {
   174  		return nil, err
   175  	}
   176  	return getDefaultStorageClass(storageClasses), nil
   177  }
   178  
   179  func getStorageClassOverride(controller *Controller, className *string) (*storagev1.StorageClass, error) {
   180  	if className != nil {
   181  		// If a storage class was explicitly specified via the VMO API, use that
   182  		return getStorageClassByName(controller, *className)
   183  	} else if controller.operatorConfig.Pvcs.StorageClass != "" {
   184  		// if a storageclass was configured in the operator, use that
   185  		return getStorageClassByName(controller, controller.operatorConfig.Pvcs.StorageClass)
   186  	}
   187  	return nil, nil
   188  }
   189  
   190  func getStorageClassByName(controller *Controller, className string) (*storagev1.StorageClass, error) {
   191  	storageClass, err := controller.storageClassLister.Get(className)
   192  	if err != nil {
   193  		return nil, fmt.Errorf("failed to fetch storage class %s: %v", className, err)
   194  	}
   195  
   196  	return storageClass, err
   197  }
   198  
   199  // Parses the given storage class into a StorageClassInfo objects
   200  func parseStorageClassInfo(storageClass *storagev1.StorageClass, operatorConfig *config.OperatorConfig) StorageClassInfo {
   201  	pvcAcceptsZone := false
   202  	pvcZoneMatchLabel := ""
   203  
   204  	if storageClass.Provisioner == constants.OciFlexVolumeProvisioner { // Special case - we already know how to handle the OCI flex volume storage class
   205  		pvcAcceptsZone = true
   206  		pvcZoneMatchLabel = constants.OciAvailabilityDomainLabel
   207  	} else if operatorConfig.Pvcs.ZoneMatchLabel != "" { // The user has explicitly specified to use zone match labels
   208  		pvcAcceptsZone = true
   209  		pvcZoneMatchLabel = operatorConfig.Pvcs.ZoneMatchLabel
   210  	}
   211  
   212  	return StorageClassInfo{
   213  		Name:              storageClass.Name,
   214  		PvcAcceptsZone:    pvcAcceptsZone,
   215  		PvcZoneMatchLabel: pvcZoneMatchLabel,
   216  	}
   217  }
   218  
   219  // Determines the availability domain from the given PVC, if possible.
   220  func getZoneFromExistingPvc(storageClassInfo StorageClassInfo, existingPvc *corev1.PersistentVolumeClaim) string {
   221  	zone := ""
   222  
   223  	// If the StorageClass has allowed us to specify zone info on the PVC, we'll read that from the existing PVC
   224  	if storageClassInfo.PvcAcceptsZone && existingPvc.Spec.Selector != nil && existingPvc.Spec.Selector.MatchLabels != nil {
   225  		if thisZone, ok := existingPvc.Spec.Selector.MatchLabels[storageClassInfo.PvcZoneMatchLabel]; ok {
   226  			zone = thisZone
   227  		}
   228  	}
   229  	return zone
   230  }
   231  
   232  // Determines the "default" storage class from a list of storage classes.
   233  func getDefaultStorageClass(storageClasses []*storagev1.StorageClass) *storagev1.StorageClass {
   234  	for _, storageClass := range storageClasses {
   235  		if storageClass.ObjectMeta.Annotations[constants.K8sDefaultStorageClassAnnotation] == "true" ||
   236  			storageClass.ObjectMeta.Annotations[constants.K8sDefaultStorageClassBetaAnnotation] == "true" {
   237  			return storageClass
   238  		}
   239  	}
   240  	return &storagev1.StorageClass{}
   241  }