2026-07-16 01:05:33 -03:00
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
}
2026-07-16 01:22:07 -03:00
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
}
2026-07-16 01:05:33 -03:00
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
}