`assign` recorded a module and `push` sealed a random own secret where its bus credential belongs; the process crash-looped until a person ran `module issue` and pushed again, and the only warning was one line in a list printed on every push. Now assigning a module that declares a broker secret issues the credential in the same act — kept when one exists, so re-assigning rotates nothing — and when the bus cannot be reached from here the assignment says which verb to run. A push never seals a placeholder in a credential's place: a module whose bus user is unminted is refused by name, with the verb. The control plane's own user is the installer's, seeded at genesis, which the test now says. And what reads one of a module's own secrets is restarted when it changes — composed for a container or daemon that names the secret's path in its volumes, environment or env-files, so a manifest need not say it: the build machine ran on an hour-old credential because its manifest restarted it on its environment file alone (issue 206). A scheduled or run-once process is left alone; it reads afresh.
193 lines
8.6 KiB
Go
193 lines
8.6 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"github.com/novox/mesh-controller/internal/broker"
|
|
"sort"
|
|
"strings"
|
|
|
|
"github.com/novox/mesh-controller/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) {
|
|
// Held while it is recorded, so it cannot land between a converge's preview and its flip and
|
|
// be taken without ever having been previewed (novox/hq ADR 0100).
|
|
ctx, release, err := holdNodes(ctx, open, []string{node})
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
defer release()
|
|
// **Before the new assignment can unsettle a seat somebody holds only by being alone**
|
|
// (novox/hq 04-ISSUES/170): what the mesh derived so far is written down, and then the
|
|
// assignment resolves against a record rather than against a coincidence.
|
|
settled, err := recordDerivedHolders(ctx, open)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
fresh, err := open.inventory.Assign(ctx, node, module)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if !fresh {
|
|
// Nothing changed, and saying "is assigned" would read as an action. One node runs one
|
|
// of each — the module's name is the assignment's identity (novox/hq ADR 0115).
|
|
return fmt.Sprintf("%s already runs %s — one node runs one of each (ADR 0115); nothing changed",
|
|
node, module), nil
|
|
}
|
|
said := fmt.Sprintf("%s is assigned %s", node, module)
|
|
for _, line := range settled {
|
|
said += "\n " + line
|
|
}
|
|
// Its bus credential, in the same act (novox/hq issue 203): an assignment pushed before its
|
|
// credential exists delivers a process that cannot authenticate and crash-loops until somebody
|
|
// runs a second verb and a second push. Issued here when the module speaks on the bus and has
|
|
// no credential yet; kept when it has one, so re-assigning rotates nothing.
|
|
if line := issueOnAssign(ctx, open, node, module); line != "" {
|
|
said += "\n " + line
|
|
}
|
|
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) {
|
|
ctx, release, err := holdNodes(ctx, open, []string{node})
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
defer release()
|
|
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()
|
|
}
|
|
|
|
// issueOnAssign gives a newly assigned module its bus credential, the way `module issue` does, and
|
|
// says what it did in one line. Nothing for a module that declares no broker secret; nothing for one
|
|
// whose user is already minted (a credential is rotated on purpose, never by re-assigning); and when
|
|
// the bus cannot be reached from here, the line names the verb and the push that would refuse the
|
|
// module until it is run — never a silent placeholder (novox/hq issue 203).
|
|
func issueOnAssign(ctx context.Context, open *stores, node, module string) string {
|
|
inv := open.inventory
|
|
shelf, err := inv.Catalogue(ctx)
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
m, known := shelf[module]
|
|
if !known || mayIssue(m) != nil {
|
|
return ""
|
|
}
|
|
user := broker.Principal{Kind: broker.KindModule, Node: node, Module: module}.Username()
|
|
if _, minted, err := inv.BusUserHash(ctx, user); err != nil || minted {
|
|
return ""
|
|
}
|
|
busAddress, err := broker.BusAddress()
|
|
if err == nil {
|
|
err = issueOnTheNewBus(ctx, inv, m, node, busAddress)
|
|
}
|
|
if err != nil {
|
|
return fmt.Sprintf("its bus credential is not issued (%v): `module issue %s --node %s` first — "+
|
|
"`push %s` refuses to send %s until it is", err, module, node, node, module)
|
|
}
|
|
return fmt.Sprintf("its bus credential is issued and sealed to %s, and arrives with the push", node)
|
|
}
|