A module a person assigns to a machine that cannot host it — its declared capability has no detector there, as fail2ban does on a host with no firewall — made Resolve refuse the entire node, so a whole-node push refused to send the healthy modules beside it too. One module on the wrong machine took down every other module on that node. Assign already keeps such an assignment on purpose (it is what a person meant, and acts.go says so), so the fix is on the resolve/push side: a directly-assigned module the machine cannot host is left out of the closure and reported as un-applied on the Resolution, rather than refusing the set. The healthy modules still resolve, declare, and converge. A module that is *required* by something running here and cannot be hosted still refuses — that set is genuinely incoherent — so the distinction is who wanted it. assign, plan and push now name the un-applied module and the missing capability, via a shared WrongMachine message, so it is neither silently dropped nor fatal. Reconciled two tests that encoded the old whole-node refusal for directly-assigned un-hostable modules; added coverage for the healthy-modules-still-converge case and the required-un-hostable-still-refuses distinction. Claude-Session: https://claude.ai/code/session_01LrgweAeERJYBg88c5cKDzF
58 lines
2.4 KiB
Go
58 lines
2.4 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
|
|
"github.com/novox/mesh-control/internal/catalogue"
|
|
)
|
|
|
|
// The things the mesh can be asked to do, separated from how it was asked.
|
|
//
|
|
// **A surface is an adapter with no decisions in it** (novox/hq ADR 0035). The command line and
|
|
// the command API call the same functions here, so an assignment refused at one is refused at the
|
|
// other for the same reason and in the same words. The moment a surface can accept something
|
|
// another would reject, the mesh has two answers to one question and people learn which to trust.
|
|
//
|
|
// Each returns what happened as text a person can read and a caller can pass on. Neither surface
|
|
// composes its own explanation, because two explanations of one refusal drift.
|
|
|
|
// assign puts a module on a node, and says at once whether the whole set still resolves.
|
|
//
|
|
// The assignment is kept even when it does not: it is what a person meant, and the refusal is
|
|
// about the set rather than about this one. That is a decision, so it lives here rather than in
|
|
// whichever surface asked.
|
|
func assign(ctx context.Context, open *stores, node, module string) (string, error) {
|
|
if err := open.inventory.Assign(ctx, node, module); err != nil {
|
|
return "", err
|
|
}
|
|
said := fmt.Sprintf("%s is assigned %s", node, module)
|
|
plan, _, err := planFor(ctx, open, node)
|
|
if err != nil {
|
|
// Kept, and still refused. Both halves are the answer.
|
|
return said, err
|
|
}
|
|
// Kept, and cannot be hosted here. Said at once rather than discovered at push: a module whose
|
|
// capability the machine lacks is on the wrong machine, and the assignment records what a person
|
|
// meant while this line says it will not run until it moves. The rest of the node still pushes.
|
|
for _, u := range plan.Unhostable {
|
|
if u.Module != module {
|
|
continue
|
|
}
|
|
for _, c := range u.Missing {
|
|
said += "\n but " + catalogue.WrongMachine(u.Module, c, node)
|
|
}
|
|
}
|
|
return said + fmt.Sprintf("\n run `push %s` to send it", node), nil
|
|
}
|
|
|
|
// unassign takes a module off a node. What it leaves behind is the host's business: a directory
|
|
// holding anything the mesh did not put there is kept (novox/hq ADR 0030).
|
|
func unassign(ctx context.Context, open *stores, node, module string) (string, error) {
|
|
if err := open.inventory.Unassign(ctx, node, module); err != nil {
|
|
return "", err
|
|
}
|
|
return fmt.Sprintf("%s no longer runs %s — run `push %s` to make it so",
|
|
node, module, node), nil
|
|
}
|