Refuse an apply that would remove a resource of a module a want still places here, so a composition that lost what runs can never remove the bus server or a held module again (issue 774)

This commit is contained in:
2026-10-29 21:04:17 +01:00
parent d2645a27e9
commit a7acee3eed
4 changed files with 129 additions and 31 deletions
+46
View File
@@ -312,6 +312,27 @@ type Scope struct {
Module string
// Others are the resources of every other module the node has a composition for.
Others []declaration.Resource
// Placed are the modules a want still places on the node (present, not removed), held ones included: no apply but
// the module's own removes a resource of one of them (issue 774: on 2026-10-29 the node's own apply on the bus node,
// composed without what its folded modules ran, removed the bus server and four held modules). Nil guards nothing.
Placed map[string]bool
}
// placedOwner is the module a want still places on the node that the record belongs to, other than the scope's own:
// the module recorded with it, else the one its id names (a resource the node's own composition carried for a folded
// module is recorded as the node's, its id naming the module). Empty when there is none.
func (sc *Scope) placedOwner(a store.Applied) string {
if sc == nil || len(sc.Placed) == 0 {
return ""
}
m := a.Module
if m == "" || m == store.NodeModule {
m, _ = declaration.ModuleGuess(a.ID)
}
if m == "" || m == sc.Module || !sc.Placed[m] {
return ""
}
return m
}
// owns reports whether a record belongs to the scope's module.
@@ -441,6 +462,31 @@ func applyScoped(
}
}
// **A module a want still places here is never removed by another's apply** (issue 774): its resources go only with
// its own apply or its want's removal. One that would is refused whole, before anything is changed, naming them: a
// composition that leaves them out lost what runs, and removing what runs is the worst a node can do with that.
if scope != nil {
var refused []string
for _, orphan := range known.Orphans(declared, origin) {
if !scope.owns(orphan) {
continue
}
if _, kept := heldByOthers[orphan.ID]; kept {
continue
}
if m := scope.placedOwner(orphan); m != "" {
refused = append(refused, orphan.ID+" of "+m)
}
}
if len(refused) > 0 {
sort.Strings(refused)
err := fmt.Errorf("the apply of %s would remove %s, of modules a want still places here; a module's resources "+
"go only with its own apply or its want's removal, so nothing was applied", scope.Module, strings.Join(refused, ", "))
log(" refused: " + err.Error())
return report, known, &Error{Resource: "the resources of modules placed here", Err: err, Done: report}
}
}
removeOrphan := func(orphan store.Applied) error {
var action, detail string
var err error