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 }