soteria/internal/server/backup_safety.go
2026-07-16 01:05:33 -03:00

148 lines
3.8 KiB
Go

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) 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
}