Remove an adopted node's guard and openings last on the flip, and keep them if anything failed (hq ADR 0103)

This commit is contained in:
2026-09-22 18:02:46 +02:00
parent 9033e3da98
commit 588ab71d14
2 changed files with 93 additions and 5 deletions
+28 -3
View File
@@ -114,7 +114,9 @@ func (e *Error) Unwrap() error { return e.Err }
// Removal happens FIRST, and the order is not arbitrary. A resource that leaves a declaration
// while another arrives at the same path is an ordinary rename: removing afterwards would
// delete the file that had just been written. Removing first risks losing the old state if the
// apply then fails — a recovery concern, where the other is a correctness one.
// apply then fails — a recovery concern, where the other is a correctness one. The one exception
// is what protects an adopted node, the openings and the guard: that goes last, and only when
// everything else applied (novox/hq ADR 0103).
func Apply(
ctx context.Context,
sys system.System,
@@ -159,7 +161,7 @@ func ApplyKeeping(
return report, known, &Error{Resource: "the firewall found on this machine", Err: err, Done: report}
}
for _, orphan := range known.Orphans(declared, origin) {
removeOrphan := func(orphan store.Applied) error {
var action, detail string
var err error
if declaration.Type(orphan.Type) == declaration.TypeOpening {
@@ -168,7 +170,7 @@ func ApplyKeeping(
action, detail, err = remove(ctx, sys, orphan, run)
}
if err != nil {
return report, known, &Error{Resource: orphan.ID, Err: err, Done: report}
return &Error{Resource: orphan.ID, Err: err, Done: report}
}
known.Forget(orphan.ID)
report.Outcomes = append(report.Outcomes, Outcome{
@@ -176,6 +178,24 @@ func ApplyKeeping(
Action: action, Detail: detail,
})
log(fmt.Sprintf(" %s %s (%s)", action, orphan.ID, orphan.Target))
return nil
}
// **What protects an adopted node goes last** (novox/hq ADR 0103). The openings and the guard
// are what keep the mesh reachable through the found firewall and the store unreachable from
// outside. When a node is converged they leave the declaration, and removing them first would
// leave the store open from the moment the guard stops until the derived filter loads — and
// for ever, if the filter then fails. So they are removed only once everything else applied
// and the found firewall is retired; if anything failed, they stay, recorded, for the next try.
var protecting []store.Applied
for _, orphan := range known.Orphans(declared, origin) {
if strings.HasPrefix(orphan.ID, declaration.AdoptionPrefix) {
protecting = append(protecting, orphan)
continue
}
if err := removeOrphan(orphan); err != nil {
return report, known, err
}
}
// What moved in this apply, so a service that must reflect a file can be told the file
@@ -321,6 +341,11 @@ func ApplyKeeping(
if err := retireFirewall(ctx, d, origin, &known, run, log); err != nil {
return report, known, &Error{Resource: "the firewall found on this machine", Err: err, Done: report}
}
for _, orphan := range protecting {
if err := removeOrphan(orphan); err != nil {
return report, known, err
}
}
}
if len(failures) > 0 {