v3.1.0 d676137

volume.go RawHistoryFolder

1package pruneguard
2
3import (
4	"sort"
5
6	"github.com/bnema/gordon/internal/domain"
7)
8
9// VolumePlanInput is the complete input of one volume plan.
10type VolumePlanInput struct {
11	Volumes  []*domain.VolumeInfo
12	Snapshot *domain.PruneProtectionSnapshot
13	// Complete is false when the runtime volume inventory could not be
14	// read in full. Candidates that would otherwise be eligible become
15	// unknown.
16	Complete bool
17}
18
19// PlanVolumes computes one verdict per runtime volume.
20//
21// A volume is eligible only when a durable ownership record explicitly
22// records it as released, the runtime labels agree with that record, and
23// no container uses it. Labels alone never make a volume deletable, and
24// a retained record is never reinterpreted as released. A legitimately
25// empty result is a success, not an error.
26//
27// Output is ordered by volume name so callers are deterministic.
28func PlanVolumes(input VolumePlanInput) []domain.VolumeVerdict {
29	ownershipIncomplete := input.Snapshot.HasGapFor(domain.InventorySourceOwnership)
30
31	verdicts := make([]domain.VolumeVerdict, 0, len(input.Volumes))
32	for _, volume := range input.Volumes {
33		verdicts = append(verdicts, planVolume(volume, input, ownershipIncomplete))
34	}
35	sort.SliceStable(verdicts, func(i, j int) bool {
36		return verdicts[i].Ref.Name < verdicts[j].Ref.Name
37	})
38	return verdicts
39}
40
41func planVolume(volume *domain.VolumeInfo, input VolumePlanInput, ownershipIncomplete bool) domain.VolumeVerdict {
42	verdict := domain.VolumeVerdict{}
43	if volume == nil {
44		verdict.Verdict, verdict.Reasons = unknownResult(domain.PruneReasonUnknownIdentity)
45		return verdict
46	}
47	verdict.Ref = domain.RuntimeVolumeRef{Name: volume.Name}
48
49	if !verdict.Ref.Valid() {
50		verdict.Verdict, verdict.Reasons = unknownResult(domain.PruneReasonUnknownIdentity)
51		return verdict
52	}
53
54	if volume.InUse {
55		verdict.Verdict, verdict.Reasons = protected(nil, domain.PruneReasonProtectedContainerUse)
56		return verdict
57	}
58
59	claim, hasClaim := input.Snapshot.VolumeClaimFor(volume.Name)
60
61	// Positive protection first: any attached or retained record wins,
62	// even when the rest of the inventory is incomplete, and a newer
63	// incarnation never un-protects an older claim.
64	if input.Snapshot.VolumeProtects(volume.Name) {
65		verdict.Verdict, verdict.Reasons = protected(nil, domain.PruneReasonProtectedOwnership)
66		return verdict
67	}
68
69	if !hasClaim {
70		switch ClassifyResource(volume.Labels) {
71		case ProvenanceUnmanaged:
72			verdict.Verdict, verdict.Reasons = protected(nil, domain.PruneReasonProtectedUnmanaged)
73		case ProvenanceLegacyManaged:
74			verdict.Verdict, verdict.Reasons = protected(nil, domain.PruneReasonProtectedManagedOnly)
75		case ProvenanceServiceManaged:
76			verdict.Verdict, verdict.Reasons = protected(nil, domain.PruneReasonProtectedServiceManaged)
77		default:
78			// App labels with no durable record: ownership history is
79			// missing, and labels never override that.
80			verdict.Verdict, verdict.Reasons = unknownResult(domain.PruneReasonUnknownProvenance)
81		}
82		return verdict
83	}
84
85	// Released: the durable record and the runtime labels must agree on
86	// the owning app incarnation before the volume can be deleted.
87	if !claim.MatchesLabels(volume.Labels) {
88		verdict.Verdict, verdict.Reasons = unknownResult(domain.PruneReasonUnknownProvenance)
89		return verdict
90	}
91
92	if ownershipIncomplete || !input.Complete {
93		verdict.Verdict, verdict.Reasons = unknownResult(domain.PruneReasonUnknownInventory)
94		return verdict
95	}
96	if !claim.Valid() {
97		verdict.Verdict, verdict.Reasons = unknownResult(domain.PruneReasonUnknownProvenance)
98		return verdict
99	}
100	// Eligibility needs both halves of the proof: a durable released
101	// record and the incarnation UUID that ties the runtime volume to
102	// it. A record without an incarnation cannot prove which volume it
103	// claims, so it never authorizes deletion.
104	if claim.AppID == "" {
105		verdict.Verdict, verdict.Reasons = unknownResult(domain.PruneReasonUnknownProvenance)
106		return verdict
107	}
108
109	verdict.Verdict, verdict.Reasons = eligibleResult(domain.PruneReasonEligibleReleasedVolume)
110	return verdict
111}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 7

Tags 106

↑↓ moveenter openesc close