package main import ( "context" "fmt" "sort" "strings" "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 what in the mesh no longer works out. // // The assignment is kept even when the node does not resolve: it is what a person meant, and // assignment is not an ordering. A consumer assigned before its provider does not resolve for as // long as it takes to assign the provider, and refusing the first half of a pair would make the // order somebody types two commands in part of the mesh's rules. // // **What is checked is the mesh, not the one machine** — because that is what the assignment // changes. novox/hq 04-ISSUES/017 names the shape: an action can succeed into a state its own // verify rejects, and it happens when the action's test is not the test the verify uses. Here the // action tested one node and the verify is resolution over all of them, so an assignment could be // reported as fine while it took a provision away from every other machine — a module offering a // mesh-scoped provision stops offering it the moment its own node stops resolving, and every // consumer elsewhere is then told *nothing in this mesh provides it*, with a remedy that names a // module already assigned. That was found on a four-node raise and read as a version bump breaking // provider recognition; it was neither the version nor the provider. // // It costs a resolution per machine. Assignment is a person typing a command, and being told which // machines this just blocked is worth more than the milliseconds. 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, and the rest of the mesh is still // worth reporting: this machine's refusal is rarely the only consequence. return said + blockedElsewhere(ctx, open, node), 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) + blockedElsewhere(ctx, open, 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). // // It reports the rest of the mesh for the same reason assign does, and more sharply: taking a // module off one machine is the ordinary way to stop providing something to another, and nothing // about the command's own output would ever have said so. 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) + blockedElsewhere(ctx, open, node), nil } // blockedElsewhere is every OTHER machine that cannot be worked out as things now stand. // // **The state, not the cause.** Saying "this assignment broke laptop" would mean resolving the // whole mesh twice and would still be a guess about which of several changes did it; saying // "laptop cannot be worked out, and here is what it says" is true, is what somebody has to fix, // and cannot mislead. The machine that was just changed is left out because its own refusal is // already the answer beside this one. // // Nothing here can fail the act it reports on. A mesh that cannot be read is worth saying and is // not a reason to claim the assignment did not happen — it did. func blockedElsewhere(ctx context.Context, open *stores, except string) string { nodes, err := open.inventory.Nodes(ctx) if err != nil { return "\n\nThe rest of the mesh could not be checked: " + err.Error() } blocked := map[string]string{} for _, n := range nodes { if n.Name == except { continue } if _, _, err := planFor(ctx, open, n.Name); err != nil { blocked[n.Name] = err.Error() } } if len(blocked) == 0 { return "" } names := make([]string, 0, len(blocked)) for name := range blocked { names = append(names, name) } sort.Strings(names) var out strings.Builder fmt.Fprintf(&out, "\n\nAND %d other machine(s) cannot be worked out as things stand, so "+ "nothing will be sent to them:\n", len(names)) for _, name := range names { fmt.Fprintf(&out, " %s\n", name) for _, line := range strings.Split(strings.TrimRight(blocked[name], "\n"), "\n") { fmt.Fprintf(&out, " %s\n", strings.TrimSpace(line)) } } out.WriteString("\nThis may or may not be what just changed — it is what is true now.") return out.String() }