package server import ( "context" "fmt" "sort" "strings" "time" "scm.bstein.dev/bstein/soteria/internal/longhorn" ) const ( longhornLabelNamespace = "soteria.bstein.dev/namespace" longhornLabelPVC = "soteria.bstein.dev/pvc" ) func (s *Server) pruneLonghornBackups(ctx context.Context, volumeName, namespace, pvc string, keepLast int) error { if keepLast <= 0 { return nil } backups, err := s.longhorn.ListBackups(ctx, volumeName) if err != nil { if longhorn.IsNotFound(err) { return nil } return err } completed := make([]longhorn.Backup, 0, len(backups)) toDelete := []longhorn.Backup{} for _, backup := range backups { if !longhornBackupMatchesPVC(backup, namespace, pvc) { continue } switch strings.ToLower(strings.TrimSpace(backup.State)) { case "completed": completed = append(completed, backup) case "error", "failed": toDelete = append(toDelete, backup) } } sort.Slice(completed, func(i, j int) bool { left := longhornBackupCreatedAt(completed[i]) right := longhornBackupCreatedAt(completed[j]) if left.Equal(right) { return completed[i].Name > completed[j].Name } return left.After(right) }) if len(completed) > keepLast { toDelete = append(toDelete, completed[keepLast:]...) } for _, backup := range toDelete { if strings.TrimSpace(backup.Name) == "" { continue } if err := s.longhorn.DeleteBackup(ctx, volumeName, backup.Name); err != nil { return fmt.Errorf("delete old Longhorn backup %s for %s/%s: %w", backup.Name, namespace, pvc, err) } } return nil } func longhornBackupMatchesPVC(backup longhorn.Backup, namespace, pvc string) bool { return backup.Labels[longhornLabelNamespace] == namespace && backup.Labels[longhornLabelPVC] == pvc } func longhornBackupCreatedAt(backup longhorn.Backup) time.Time { created := strings.TrimSpace(backup.Created) if created == "" { return time.Time{} } ts, err := time.Parse(time.RFC3339, created) if err != nil { return time.Time{} } return ts }