148 lines
3.8 KiB
Go
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
|
|
}
|