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: &params.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  }