guard.go RawHistoryFolder
1// Package pruneguard holds the ownership-aware prune policy: pure
2// planning from complete inputs to per-candidate verdicts. Every
3// exported function here is pure — inputs in, verdicts out, no store
4// reads, no runtime calls, no deletion.
5//
6// The policy is fail-closed per candidate, never per operation:
7//
8// - A resource is eligible only when every fact needed to prove it
9// safe was read completely and no durable root claims it.
10// - A durable root (desired/active/recovery/apply/operation/ownership
11// /container use/pending upload/shared closure/pin) makes the
12// candidate protected.
13// - Anything else — unknown identity, unknown provenance, incomplete
14// inventory, unreadable manifest — makes the candidate unknown.
15// Unknown candidates are never deleted; they do not disable the
16// whole operation.
17//
18// Ownership is never inferred from names, and labels never override a
19// contradictory or missing durable ownership record.
20package pruneguard
21
22import (
23 "github.com/bnema/gordon/internal/domain"
24)
25
26// Provenance classifies a runtime resource by the labels it carries.
27type Provenance int
28
29const (
30 // ProvenanceUnmanaged carries no Gordon labels: never adopted,
31 // never deleted.
32 ProvenanceUnmanaged Provenance = iota
33 // ProvenanceLegacyManaged carries the managed label but no app
34 // ownership label: the legacy/unknown class.
35 ProvenanceLegacyManaged
36 // ProvenanceServiceManaged carries standalone service labels.
37 ProvenanceServiceManaged
38 // ProvenanceAppOwned carries app ownership labels.
39 ProvenanceAppOwned
40)
41
42// ClassifyResource maps a label set to its provenance. App ownership
43// wins over service labels, and the managed marker alone is never
44// treated as app ownership.
45func ClassifyResource(labels map[string]string) Provenance {
46 if len(labels) == 0 {
47 return ProvenanceUnmanaged
48 }
49 if labels[domain.LabelApp] != "" {
50 return ProvenanceAppOwned
51 }
52 if labels[domain.LabelService] != "" || labels[domain.LabelServiceName] != "" {
53 return ProvenanceServiceManaged
54 }
55 if labels[domain.LabelManaged] == "true" {
56 return ProvenanceLegacyManaged
57 }
58 return ProvenanceUnmanaged
59}
60
61// protected builds a protected verdict with canonical reasons, falling
62// back to a reason that always fits when no root kind mapped.
63func protected(reasons []domain.PruneReason, fallback domain.PruneReason) (domain.PruneVerdict, []domain.PruneReason) {
64 canonical := domain.CanonicalReasons(reasons...)
65 if len(canonical) == 0 {
66 canonical = domain.CanonicalReasons(fallback)
67 }
68 return domain.PruneVerdictProtected, canonical
69}
70
71// eligibleResult builds an eligible verdict with its single reason.
72func eligibleResult(reason domain.PruneReason) (domain.PruneVerdict, []domain.PruneReason) {
73 return domain.PruneVerdictEligible, domain.CanonicalReasons(reason)
74}
75
76// unknownResult builds an unknown verdict with its single reason.
77func unknownResult(reason domain.PruneReason) (domain.PruneVerdict, []domain.PruneReason) {
78 return domain.PruneVerdictUnknown, domain.CanonicalReasons(reason)
79}
80
81// rootReasonsOrFallback collects the mapped reasons of every root
82// claiming refs, falling back to a shared-content reason when a root
83// matched but its kind cannot justify a verdict reason.
84func rootReasonsOrFallback(snapshot *domain.PruneProtectionSnapshot, refs ...string) (bool, []domain.PruneReason) {
85 if snapshot == nil {
86 return false, nil
87 }
88 matched := false
89 for _, ref := range refs {
90 if ref != "" && snapshot.ProtectsAnyRef(ref) {
91 matched = true
92 break
93 }
94 }
95 if !matched {
96 return false, nil
97 }
98 reasons := snapshot.RootReasons(refs...)
99 if len(reasons) == 0 {
100 reasons = domain.CanonicalReasons(domain.PruneReasonProtectedSharedContent)
101 }
102 return true, reasons
103}