github.com/juju/juju@v0.0.0-20240430160146-1752b71fcf00/caas/kubernetes/provider/storage/storage.go (about) 1 // Copyright 2020 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package storage 5 6 import ( 7 "context" 8 "fmt" 9 "time" 10 11 "github.com/juju/errors" 12 corev1 "k8s.io/api/core/v1" 13 storagev1 "k8s.io/api/storage/v1" 14 "k8s.io/apimachinery/pkg/api/resource" 15 "k8s.io/client-go/kubernetes" 16 17 "github.com/juju/juju/caas" 18 k8s "github.com/juju/juju/caas/kubernetes" 19 "github.com/juju/juju/caas/kubernetes/provider/constants" 20 "github.com/juju/juju/caas/kubernetes/provider/resources" 21 "github.com/juju/juju/caas/kubernetes/provider/utils" 22 "github.com/juju/juju/core/status" 23 "github.com/juju/juju/storage" 24 storageprovider "github.com/juju/juju/storage/provider" 25 ) 26 27 // GetMountPathForFilesystem returns mount path. 28 func GetMountPathForFilesystem(idx int, appName string, fs storage.KubernetesFilesystemParams) string { 29 if fs.Attachment != nil { 30 return fs.Attachment.Path 31 } 32 return fmt.Sprintf("%s/fs/%s/%s/%d", constants.StorageBaseDir, appName, fs.StorageName, idx) 33 } 34 35 // FilesystemStatus returns filesystem status. 36 func FilesystemStatus(pvcPhase corev1.PersistentVolumeClaimPhase) status.Status { 37 switch pvcPhase { 38 case corev1.ClaimPending: 39 return status.Pending 40 case corev1.ClaimBound: 41 return status.Attached 42 case corev1.ClaimLost: 43 return status.Detached 44 default: 45 return status.Unknown 46 } 47 } 48 49 // VolumeStatus returns volume status. 50 func VolumeStatus(pvPhase corev1.PersistentVolumePhase) status.Status { 51 switch pvPhase { 52 case corev1.VolumePending: 53 return status.Pending 54 case corev1.VolumeBound: 55 return status.Attached 56 case corev1.VolumeAvailable, corev1.VolumeReleased: 57 return status.Detached 58 case corev1.VolumeFailed: 59 return status.Error 60 default: 61 return status.Unknown 62 } 63 } 64 65 // VolumeSourceForFilesystem return k8s volume source. 66 func VolumeSourceForFilesystem(fs storage.KubernetesFilesystemParams) (*corev1.VolumeSource, error) { 67 fsSize, err := resource.ParseQuantity(fmt.Sprintf("%dMi", fs.Size)) 68 if err != nil { 69 return nil, errors.Annotatef(err, "invalid volume size %v", fs.Size) 70 } 71 switch fs.Provider { 72 case constants.StorageProviderType: 73 return nil, nil 74 case storageprovider.RootfsProviderType: 75 return &corev1.VolumeSource{ 76 EmptyDir: &corev1.EmptyDirVolumeSource{ 77 SizeLimit: &fsSize, 78 }, 79 }, nil 80 case storageprovider.TmpfsProviderType: 81 medium, ok := fs.Attributes[constants.StorageMedium] 82 if !ok { 83 medium = corev1.StorageMediumMemory 84 } 85 return &corev1.VolumeSource{ 86 EmptyDir: &corev1.EmptyDirVolumeSource{ 87 Medium: corev1.StorageMedium(fmt.Sprintf("%v", medium)), 88 SizeLimit: &fsSize, 89 }, 90 }, nil 91 default: 92 return nil, errors.NotValidf("charm storage provider type %q for %v", fs.Provider, fs.StorageName) 93 } 94 } 95 96 // StorageClassSpec converts storage provisioner config to k8s storage class. 97 func StorageClassSpec(cfg k8s.StorageProvisioner, legacyLabels bool) *storagev1.StorageClass { 98 sc := storagev1.StorageClass{} 99 sc.Name = constants.QualifiedStorageClassName(cfg.Namespace, cfg.Name) 100 sc.Provisioner = cfg.Provisioner 101 sc.Parameters = cfg.Parameters 102 if cfg.ReclaimPolicy != "" { 103 policy := corev1.PersistentVolumeReclaimPolicy(cfg.ReclaimPolicy) 104 sc.ReclaimPolicy = &policy 105 } 106 if cfg.VolumeBindingMode != "" { 107 bindMode := storagev1.VolumeBindingMode(cfg.VolumeBindingMode) 108 sc.VolumeBindingMode = &bindMode 109 } 110 if cfg.Model != "" { 111 sc.Labels = utils.LabelsForModel(cfg.Model, legacyLabels) 112 } 113 return &sc 114 } 115 116 // VolumeInfo returns volume info. 117 func VolumeInfo(pv *resources.PersistentVolume, now time.Time) caas.VolumeInfo { 118 size := quantityAsMibiBytes(*pv.Spec.Capacity.Storage()) 119 return caas.VolumeInfo{ 120 VolumeId: pv.Name, 121 Size: size, 122 Persistent: true, 123 Status: status.StatusInfo{ 124 Status: VolumeStatus(pv.Status.Phase), 125 Message: pv.Status.Message, 126 Since: &now, 127 }, 128 } 129 } 130 131 // FilesystemInfo returns filesystem info. 132 func FilesystemInfo(ctx context.Context, client kubernetes.Interface, 133 namespace string, volume corev1.Volume, volumeMount corev1.VolumeMount, 134 now time.Time) (*caas.FilesystemInfo, error) { 135 if volume.EmptyDir != nil { 136 size := uint64(0) 137 if volume.EmptyDir.SizeLimit != nil { 138 size = quantityAsMibiBytes(*volume.EmptyDir.SizeLimit) 139 } 140 return &caas.FilesystemInfo{ 141 Size: size, 142 FilesystemId: volume.Name, 143 MountPoint: volumeMount.MountPath, 144 ReadOnly: volumeMount.ReadOnly, 145 Status: status.StatusInfo{ 146 Status: status.Attached, 147 Since: &now, 148 }, 149 Volume: caas.VolumeInfo{ 150 VolumeId: volume.Name, 151 Size: size, 152 Persistent: false, 153 Status: status.StatusInfo{ 154 Status: status.Attached, 155 Since: &now, 156 }, 157 }, 158 }, nil 159 } else if volume.PersistentVolumeClaim == nil || volume.PersistentVolumeClaim.ClaimName == "" { 160 // Ignore volumes which are not Juju managed filesystems. 161 logger.Debugf("ignoring blank EmptyDir, PersistentVolumeClaim or ClaimName") 162 return nil, errors.NotSupportedf("volume %v", volume) 163 } 164 165 // Handle PVC 166 pvc := resources.NewPersistentVolumeClaim(volume.PersistentVolumeClaim.ClaimName, namespace, nil) 167 err := pvc.Get(ctx, client) 168 if err != nil { 169 return nil, errors.Annotate(err, "unable to get persistent volume claim") 170 } 171 172 if pvc.Status.Phase == corev1.ClaimPending { 173 logger.Debugf(fmt.Sprintf("PersistentVolumeClaim for %v is pending", pvc.Name)) 174 return nil, nil 175 } 176 177 storageName := utils.StorageNameFromLabels(pvc.Labels) 178 if storageName == "" { 179 if valid := constants.LegacyPVNameRegexp.MatchString(volumeMount.Name); valid { 180 storageName = constants.LegacyPVNameRegexp.ReplaceAllString(volumeMount.Name, "$storageName") 181 } else if valid := constants.PVNameRegexp.MatchString(volumeMount.Name); valid { 182 storageName = constants.PVNameRegexp.ReplaceAllString(volumeMount.Name, "$storageName") 183 } 184 } 185 186 statusMessage := "" 187 since := now 188 if len(pvc.Status.Conditions) > 0 { 189 statusMessage = pvc.Status.Conditions[0].Message 190 since = pvc.Status.Conditions[0].LastProbeTime.Time 191 } 192 if statusMessage == "" { 193 // If there are any events for this pvc we can use the 194 // most recent to set the status. 195 eventList, err := pvc.Events(ctx, client) 196 if err != nil { 197 return nil, errors.Annotate(err, "unable to get events for PVC") 198 } 199 // Take the most recent event. 200 if count := len(eventList); count > 0 { 201 statusMessage = eventList[count-1].Message 202 } 203 } 204 205 pv := resources.NewPersistentVolume(pvc.Spec.VolumeName, nil) 206 err = pv.Get(ctx, client) 207 if errors.IsNotFound(err) { 208 // Ignore volumes which don't exist (yet). 209 return nil, nil 210 } 211 if err != nil { 212 return nil, errors.Annotate(err, "unable to get persistent volume") 213 } 214 215 return &caas.FilesystemInfo{ 216 StorageName: storageName, 217 Size: quantityAsMibiBytes(*pvc.Spec.Resources.Requests.Storage()), 218 FilesystemId: string(pvc.UID), 219 MountPoint: volumeMount.MountPath, 220 ReadOnly: volumeMount.ReadOnly, 221 Status: status.StatusInfo{ 222 Status: FilesystemStatus(pvc.Status.Phase), 223 Message: statusMessage, 224 Since: &since, 225 }, 226 Volume: VolumeInfo(pv, since), 227 }, nil 228 } 229 230 // PersistentVolumeClaimSpec returns k8s PVC spec. 231 func PersistentVolumeClaimSpec(params VolumeParams) *corev1.PersistentVolumeClaimSpec { 232 return &corev1.PersistentVolumeClaimSpec{ 233 StorageClassName: ¶ms.StorageConfig.StorageClass, 234 Resources: corev1.VolumeResourceRequirements{ 235 Requests: corev1.ResourceList{ 236 corev1.ResourceStorage: params.Size, 237 }, 238 }, 239 AccessModes: []corev1.PersistentVolumeAccessMode{params.AccessMode}, 240 } 241 } 242 243 // StorageProvisioner returns storage provisioner. 244 func StorageProvisioner(namespace, model string, params VolumeParams) k8s.StorageProvisioner { 245 return k8s.StorageProvisioner{ 246 Name: params.StorageConfig.StorageClass, 247 Namespace: namespace, 248 Model: model, 249 Provisioner: params.StorageConfig.StorageProvisioner, 250 Parameters: params.StorageConfig.Parameters, 251 ReclaimPolicy: string(params.StorageConfig.ReclaimPolicy), 252 } 253 } 254 255 func quantityAsMibiBytes(q resource.Quantity) uint64 { 256 return uint64(q.MilliValue()) / 1000 / 1024 / 1024 257 }