soteria/internal/server/longhorn_retention.go

85 lines
2.0 KiB
Go

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
}