package server import ( "context" "fmt" "path" "sort" "strings" "scm.bstein.dev/bstein/soteria/internal/k8s" corev1 "k8s.io/api/core/v1" ) func (s *Server) pvcSummaryExcluded(pvc k8s.PVCSummary) (bool, string) { return s.pvcExcluded(pvc.Namespace, pvc.Name, pvc.StorageClass) } func (s *Server) pvcExcluded(namespace, pvc, storageClass string) (bool, string) { storageClass = strings.TrimSpace(storageClass) for _, excluded := range s.cfg.ExcludedStorageClasses { if storageClass != "" && strings.EqualFold(storageClass, strings.TrimSpace(excluded)) { return true, fmt.Sprintf("storage class %s is excluded from restic backups", storageClass) } } key := strings.Trim(strings.TrimSpace(namespace)+"/"+strings.TrimSpace(pvc), "/") for _, pattern := range s.cfg.ExcludedPVCs { pattern = strings.TrimSpace(pattern) if pattern == "" { continue } if pattern == key { return true, fmt.Sprintf("PVC %s is excluded from restic backups", key) } matched, err := path.Match(pattern, key) if err == nil && matched { return true, fmt.Sprintf("PVC %s matches excluded pattern %s", key, pattern) } } return false, "" } func (s *Server) liveExclusivePVCMounted(ctx context.Context, namespace, pvc string, accessModes []string) (bool, string, error) { if !hasExclusiveAccessMode(accessModes) { return false, "", nil } mounts, err := s.client.ListPVCMounts(ctx, namespace, pvc) if err != nil { return false, "", err } for _, mount := range mounts { if mount.SoteriaBackup { continue } location := strings.TrimSpace(mount.PodName) if mount.NodeName != "" { location += " on " + mount.NodeName } return true, fmt.Sprintf("RWO PVC %s/%s is mounted by active pod %s; restic backups wait for the workload to release the claim or use Longhorn snapshots", namespace, pvc, location), nil } return false, "", nil } func hasExclusiveAccessMode(accessModes []string) bool { for _, mode := range accessModes { switch corev1.PersistentVolumeAccessMode(strings.TrimSpace(mode)) { case corev1.ReadWriteOnce, corev1.ReadWriteOncePod: return true } } return false } func pvcAccessModes(pvc *corev1.PersistentVolumeClaim) []string { if pvc == nil { return nil } modes := make([]string, 0, len(pvc.Spec.AccessModes)) for _, mode := range pvc.Spec.AccessModes { modes = append(modes, string(mode)) } return modes } func (s *Server) longhornVolumeReady(ctx context.Context, volumeName string) (bool, string, error) { volume, err := s.longhorn.GetVolume(ctx, volumeName) if err != nil { return false, "", err } state := strings.ToLower(strings.TrimSpace(volume.State)) if state != "" && state != "attached" { return false, fmt.Sprintf("Longhorn volume %s is %s; backup waits until the volume is attached", volumeName, state), nil } return true, "", nil } func (s *Server) activeResticRepositories(ctx context.Context, namespaces []string) (map[string]struct{}, error) { active := map[string]struct{}{} for _, namespace := range uniqueSortedStrings(namespaces) { jobs, err := s.client.ListBackupJobs(ctx, namespace) if err != nil { return nil, err } for _, job := range jobs { if !backupJobInProgress(job.State) { continue } repository := strings.TrimSpace(job.Repository) if repository == "" { repository = strings.TrimSpace(s.cfg.ResticRepository) } if repository != "" { active[repository] = struct{}{} } } } return active, nil } func (s *Server) resticRepositoryBusy(ctx context.Context, repository string) (bool, error) { repository = strings.TrimSpace(repository) if repository == "" { return false, nil } pvcs, err := s.client.ListBoundPVCs(ctx) if err != nil { return false, err } namespaces := make([]string, 0, len(pvcs)) for _, pvc := range pvcs { namespaces = append(namespaces, pvc.Namespace) } active, err := s.activeResticRepositories(ctx, namespaces) if err != nil { return false, err } _, busy := active[repository] return busy, nil } func uniqueSortedStrings(items []string) []string { seen := map[string]struct{}{} for _, item := range items { item = strings.TrimSpace(item) if item == "" { continue } seen[item] = struct{}{} } out := make([]string, 0, len(seen)) for item := range seen { out = append(out, item) } sort.Strings(out) return out }