1package backup
2
3import (
4 "sort"
5 "strings"
6
7 "github.com/bnema/gordon/internal/domain"
8)
9
10// SelectVolumeBackupTargets returns deterministic volume backup targets from Gordon-managed containers.
11func SelectVolumeBackupTargets(containers []*domain.Container, volumePrefix string) []domain.VolumeBackupTarget {
12 return SelectVolumeBackupTargetsForScope(containers, volumePrefix, "", "")
13}
14
15// SelectVolumeBackupTargetsForScope returns deterministic volume backup targets after applying domain/volume filters.
16func SelectVolumeBackupTargetsForScope(containers []*domain.Container, volumePrefix, domainFilter, volumeFilter string) []domain.VolumeBackupTarget {
17 targets := make([]domain.VolumeBackupTarget, 0)
18 seenVolumes := make(map[string]struct{})
19
20 for _, c := range sortedVolumeBackupContainers(containers) {
21 domainName, ok := selectedVolumeBackupContainerDomain(c, domainFilter)
22 if !ok {
23 continue
24 }
25 for _, mount := range selectedVolumeBackupMounts(c.VolumeMounts, volumePrefix, volumeFilter) {
26 if _, ok := seenVolumes[mount.Name]; ok {
27 continue
28 }
29 seenVolumes[mount.Name] = struct{}{}
30 targets = append(targets, domain.VolumeBackupTarget{
31 Domain: domainName,
32 ContainerName: c.Name,
33 ContainerID: c.ID,
34 VolumeName: mount.Name,
35 MountPath: mount.Destination,
36 })
37 }
38 }
39
40 return targets
41}
42
43func sortedVolumeBackupContainers(containers []*domain.Container) []*domain.Container {
44 sorted := make([]*domain.Container, 0, len(containers))
45 for _, c := range containers {
46 if c != nil {
47 sorted = append(sorted, c)
48 }
49 }
50 sort.Slice(sorted, func(i, j int) bool {
51 leftDomain := volumeBackupDomain(sorted[i])
52 rightDomain := volumeBackupDomain(sorted[j])
53 if leftDomain != rightDomain {
54 return leftDomain < rightDomain
55 }
56 return sorted[i].Name < sorted[j].Name
57 })
58 return sorted
59}
60
61func selectedVolumeBackupContainerDomain(c *domain.Container, domainFilter string) (string, bool) {
62 if c.Labels == nil || c.Labels[domain.LabelManaged] != "true" {
63 return "", false
64 }
65 domainName := volumeBackupDomain(c)
66 if domainName == "" || (domainFilter != "" && domainName != domainFilter) {
67 return "", false
68 }
69 return domainName, true
70}
71
72func selectedVolumeBackupMounts(mounts []domain.ContainerVolumeMount, volumePrefix, volumeFilter string) []domain.ContainerVolumeMount {
73 selected := append([]domain.ContainerVolumeMount(nil), mounts...)
74 sort.Slice(selected, func(i, j int) bool {
75 if selected[i].Name != selected[j].Name {
76 return selected[i].Name < selected[j].Name
77 }
78 return selected[i].Destination < selected[j].Destination
79 })
80 out := selected[:0]
81 for _, mount := range selected {
82 if volumeFilter != "" && mount.Name != volumeFilter {
83 continue
84 }
85 if !isEligibleVolumeMount(mount, volumePrefix) {
86 continue
87 }
88 out = append(out, mount)
89 }
90 return out
91}
92
93func volumeBackupDomain(c *domain.Container) string {
94 if c == nil || c.Labels == nil {
95 return ""
96 }
97 if c.Labels[domain.LabelAttachment] == "true" {
98 return c.Labels[domain.LabelAttachedTo]
99 }
100 if domainName := c.Labels[domain.LabelDomain]; domainName != "" {
101 return domainName
102 }
103 return c.Labels[domain.LabelRoute]
104}
105
106func isEligibleVolumeMount(mount domain.ContainerVolumeMount, volumePrefix string) bool {
107 if mount.Type != "volume" || mount.Name == "" || mount.Destination == "" {
108 return false
109 }
110 prefix := strings.TrimSpace(volumePrefix)
111 if prefix == "" {
112 return true
113 }
114 return strings.HasPrefix(mount.Name, prefix+"-")
115}