package cli import ( "context" "errors" "fmt" models "github.com/portainer/portainer/api/http/models/kubernetes" "github.com/rs/zerolog/log" appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" k8serrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" ) // GetPersistentVolumeClaims returns all PVCs in the given namespace (or all namespaces if empty). // For non-admin users, results are filtered to their accessible namespaces. func (kcl *KubeClient) GetPersistentVolumeClaims(namespace string) ([]models.K8sPersistentVolumeClaim, error) { if kcl.GetIsKubeAdmin() { return kcl.fetchPersistentVolumeClaims(namespace) } return kcl.fetchPersistentVolumeClaimsForNonAdmin(namespace) } // GetPersistentVolumeClaim returns a single PVC by namespace and name. func (kcl *KubeClient) GetPersistentVolumeClaim(namespace, name string) (*models.K8sPersistentVolumeClaim, error) { if !kcl.GetIsKubeAdmin() { allowedNamespaces := kcl.buildNonAdminNamespacesMap() if _, ok := allowedNamespaces[namespace]; !ok { return nil, fmt.Errorf("access to namespace %s is not authorized: %w", namespace, ErrUnauthorized) } } pvc, err := kcl.cli.CoreV1().PersistentVolumeClaims(namespace).Get(context.Background(), name, metav1.GetOptions{}) if err != nil { return nil, fmt.Errorf("unable to get persistent volume claim %s/%s. Error: %w", namespace, name, err) } result := parsePersistentVolumeClaimDetail(pvc) return &result, nil } // DeletePersistentVolumeClaims deletes the specified PVCs. func (kcl *KubeClient) DeletePersistentVolumeClaims(reqs models.K8sVolumeDeleteRequests) error { for _, req := range reqs { log.Debug(). Str("context", "DeletePersistentVolumeClaims"). Str("namespace", req.Namespace). Str("name", req.Name). Msg("Deleting persistent volume claim") err := kcl.cli.CoreV1().PersistentVolumeClaims(req.Namespace).Delete(context.Background(), req.Name, metav1.DeleteOptions{}) if err != nil { return fmt.Errorf("unable to delete persistent volume claim %s/%s. Error: %w", req.Namespace, req.Name, err) } } return nil } // ResizePersistentVolumeClaim resizes a PVC to the given new size. // The storage class must have AllowVolumeExpansion set to true. func (kcl *KubeClient) ResizePersistentVolumeClaim(namespace, name, newSize string) error { pvc, err := kcl.cli.CoreV1().PersistentVolumeClaims(namespace).Get(context.Background(), name, metav1.GetOptions{}) if err != nil { return fmt.Errorf("unable to get persistent volume claim %s/%s. Error: %w", namespace, name, err) } if pvc.Spec.StorageClassName != nil { sc, err := kcl.GetStorageClass(*pvc.Spec.StorageClassName) if err != nil { return fmt.Errorf("unable to get storage class %s. Error: %w", *pvc.Spec.StorageClassName, err) } if sc.AllowVolumeExpansion == nil || !*sc.AllowVolumeExpansion { return errors.New("storage class " + sc.Name + " does not allow volume expansion") } } _, err = resource.ParseQuantity(newSize) if err != nil { return fmt.Errorf("invalid size format %q. Error: %w", newSize, err) } patch := fmt.Sprintf(`{"spec":{"resources":{"requests":{"storage":"%s"}}}}`, newSize) _, err = kcl.cli.CoreV1().PersistentVolumeClaims(namespace).Patch( context.Background(), name, types.MergePatchType, []byte(patch), metav1.PatchOptions{}, ) if err != nil { return fmt.Errorf("unable to resize persistent volume claim %s/%s. Error: %w", namespace, name, err) } return nil } func (kcl *KubeClient) fetchPersistentVolumeClaims(namespace string) ([]models.K8sPersistentVolumeClaim, error) { pvcList, err := kcl.cli.CoreV1().PersistentVolumeClaims(namespace).List(context.Background(), metav1.ListOptions{}) if err != nil { return nil, fmt.Errorf("unable to list persistent volume claims. Error: %w", err) } storageClasses, err := kcl.GetStorageClasses() if err != nil { return nil, fmt.Errorf("unable to list storage classes for volume expansion lookup. Error: %w", err) } scExpansionMap := make(map[string]bool, len(storageClasses)) for _, sc := range storageClasses { if sc.AllowVolumeExpansion != nil { scExpansionMap[sc.Name] = *sc.AllowVolumeExpansion } } results := make([]models.K8sPersistentVolumeClaim, 0, len(pvcList.Items)) for i := range pvcList.Items { pvc := parsePersistentVolumeClaimDetail(&pvcList.Items[i]) if pvc.StorageClass != nil { pvc.AllowVolumeExpansion = scExpansionMap[*pvc.StorageClass] } results = append(results, pvc) } return results, nil } func (kcl *KubeClient) fetchPersistentVolumeClaimsForNonAdmin(namespace string) ([]models.K8sPersistentVolumeClaim, error) { pvcs, err := kcl.fetchPersistentVolumeClaims(namespace) if err != nil { return nil, err } nonAdminNamespaceSet := kcl.buildNonAdminNamespacesMap() results := make([]models.K8sPersistentVolumeClaim, 0) for _, pvc := range pvcs { if _, ok := nonAdminNamespaceSet[pvc.Namespace]; ok { results = append(results, pvc) } } return results, nil } // parsePersistentVolumeClaimDetail parses a PVC into the model with access modes and storage request string. func parsePersistentVolumeClaimDetail(pvc *corev1.PersistentVolumeClaim) models.K8sPersistentVolumeClaim { storage := pvc.Spec.Resources.Requests[corev1.ResourceStorage] return models.K8sPersistentVolumeClaim{ ID: string(pvc.UID), Name: pvc.Name, Namespace: pvc.Namespace, CreationDate: pvc.CreationTimestamp.Time, Storage: storage.Value(), StorageRequest: storage.String(), AccessModes: humanReadableAccessModes(pvc.Spec.AccessModes), HumanReadableAccessModes: pvc.Spec.AccessModes, VolumeName: pvc.Spec.VolumeName, ResourcesRequests: &pvc.Spec.Resources.Requests, StorageClass: pvc.Spec.StorageClassName, VolumeMode: pvc.Spec.VolumeMode, OwningApplications: nil, Phase: pvc.Status.Phase, Labels: pvc.Labels, } } // CombineClaimsWithApplications enriches each PVC with the workloads that mount it. func (kcl *KubeClient) CombineClaimsWithApplications(pvcs []models.K8sPersistentVolumeClaim) ([]models.K8sPersistentVolumeClaim, error) { pods, err := kcl.cli.CoreV1().Pods("").List(context.Background(), metav1.ListOptions{}) if err != nil { if k8serrors.IsNotFound(err) { return pvcs, nil } return nil, fmt.Errorf("failed to list pods: %w", err) } replicaSetItems := make([]appsv1.ReplicaSet, 0) deploymentItems := make([]appsv1.Deployment, 0) if containsReplicaSetOwnerReference(pods) { replicaSets, err := kcl.cli.AppsV1().ReplicaSets("").List(context.Background(), metav1.ListOptions{}) if err != nil { return nil, fmt.Errorf("failed to list replica sets: %w", err) } replicaSetItems = replicaSets.Items deployments, err := kcl.cli.AppsV1().Deployments("").List(context.Background(), metav1.ListOptions{}) if err != nil { return nil, fmt.Errorf("failed to list deployments: %w", err) } deploymentItems = deployments.Items } statefulSetItems := make([]appsv1.StatefulSet, 0) if containsStatefulSetOwnerReference(pods) { statefulSets, err := kcl.cli.AppsV1().StatefulSets("").List(context.Background(), metav1.ListOptions{}) if err != nil { return nil, fmt.Errorf("failed to list stateful sets: %w", err) } statefulSetItems = statefulSets.Items } daemonSetItems := make([]appsv1.DaemonSet, 0) if containsDaemonSetOwnerReference(pods) { daemonSets, err := kcl.cli.AppsV1().DaemonSets("").List(context.Background(), metav1.ListOptions{}) if err != nil { return nil, fmt.Errorf("failed to list daemon sets: %w", err) } daemonSetItems = daemonSets.Items } resources := PortainerApplicationResources{ ReplicaSets: replicaSetItems, Deployments: deploymentItems, StatefulSets: statefulSetItems, DaemonSets: daemonSetItems, } for i := range pvcs { for _, pod := range pods.Items { for _, podVolume := range pod.Spec.Volumes { if podVolume.PersistentVolumeClaim == nil { continue } if podVolume.PersistentVolumeClaim.ClaimName != pvcs[i].Name || pod.Namespace != pvcs[i].Namespace { continue } application, err := kcl.ConvertPodToApplication(pod, resources, false) if err != nil { return nil, fmt.Errorf("failed to convert pod to application: %w", err) } if application == nil { continue } alreadyAdded := false for _, existing := range pvcs[i].OwningApplications { if existing.Name == application.Name && existing.Namespace == application.Namespace { alreadyAdded = true break } } if !alreadyAdded { pvcs[i].OwningApplications = append(pvcs[i].OwningApplications, *application) } } } } return pvcs, nil }