package resolution import ( "fmt" "sort" "strings" mcat "git.novox.be/novox/mesh-sdk/go/catalogue" ) // heldKey is what one holder holds: the seat, and its kind on a kinded bench. func heldKey(claim, kind string) string { if kind == "" { return canonicalSeat(claim) } return canonicalSeat(claim) + "/" + kind } // Refusal is why a set of assignments cannot become a declaration. // // Every reason at once rather than the first, and each says what to do about it. A person // resolving these fixes them in one pass or in four. type Refusal struct{ Problems []string } func (r *Refusal) Error() string { return "these assignments cannot be applied:\n - " + strings.Join(r.Problems, "\n - ") } // Resolve works out everything a node runs, from what was assigned to it. // // The catalogue is every module the mesh knows about; assigned is what a person put on this node. // What comes back is the closure — assigned modules plus everything they require — or a refusal // naming every reason it could not be closed. func Resolve(catalogue map[string]mcat.Manifest, assigned []string, node mcat.Node, world mcat.World) (mcat.Resolution, error) { elsewhere := world.Held var problems []string // Which names are answered from elsewhere in the mesh rather than from this machine. A // property of the name, not of each provider: two modules disagreeing about whether a // database is local would make the same requirement mean different things depending on which // one happened to answer it. brokered := map[string]bool{} local := map[string]bool{} // And which of them keep their consumers' data (novox/hq ADR 0232) — also a property of the name. keeps := map[string]bool{} for _, m := range catalogue { for _, o := range m.Provides { if o.At() == mcat.ScopeMesh { brokered[o.Name] = true if m.KeepsConsumerData(o.Name) { keeps[o.Name] = true } continue } local[o.Name] = true } } // Which names are usable only on their provider's own machine (novox/hq ADR 0208 §3). Also a // property of the name: a display one provider says is the machine's and another says is not // would be pulled in for one consumer and refused for the next. machineReach := map[string]bool{} plainLocal := map[string]bool{} for _, m := range catalogue { for _, o := range m.Provides { if o.MachineReach() { machineReach[o.Name] = true } else if o.At() == mcat.ScopeNode { plainLocal[o.Name] = true } } } for want := range machineReach { if plainLocal[want] { problems = append(problems, fmt.Sprintf( "the catalogue disagrees about %q: some modules provide it with the machine's reach and "+ "others without, so a requirement for it would be met differently by each", want)) } } for want := range brokered { if local[want] { problems = append(problems, fmt.Sprintf( "the catalogue disagrees about %q: some modules provide it here and others from "+ "anywhere in the mesh, so the same requirement would mean two things", want)) } } // What each name can be satisfied by. Built once from the whole catalogue, because "how many // modules provide this" is the question the whole rule turns on. offers := map[string][]string{} for _, m := range catalogue { for _, o := range m.Offers() { offers[o] = append(offers[o], m.Module) } } for k := range offers { sort.Strings(offers[k]) } chosen := map[string]bool{} because := map[string]string{} var order []string var needs []mcat.Needed // What the set already offers, which is the first thing a requirement is checked against. // // Without this, assigning zsh does not satisfy something that requires a shell: the // requirement is counted against the catalogue, three modules provide it, and the answer is // still "choose one" after somebody has chosen one. That makes the remedy useless, and it is // how this read when first used. satisfied := map[string]bool{} // Which modules a person assigned here, hostable. The walk marks a module chosen only when it // reaches it, and a consumer may be reached before the provider beside it — so the provider of // something already satisfied is looked for among these as well as among the chosen. assignedHere := map[string]bool{} // Everything a person assigned goes in first, except what this machine cannot run. Those are // choices already made, and a requirement one of them answers is not a choice to put back to // anybody. // // **A module the machine cannot host is left out here rather than refusing the whole set.** // Assigning fail2ban to a machine whose profile reports no firewall is one module on the wrong // machine (novox/hq ADR 0009's sibling case): the person meant something, the remedy is theirs, // and it is a fact about this one module — not a reason the healthy modules beside it should // fail to resolve and converge. It is reported as un-applied on the resolution, so it is neither // silently dropped nor fatal to the rest. The distinction is who wanted it: a module *required* // by something running here that cannot be hosted makes the set itself incoherent, and that is // still refused, by checkCapabilities, where it stays in the closure. var unhostable []mcat.Unhostable var queue []string for _, a := range assigned { if m, known := catalogue[a]; known { if missing := missingCapabilities(m, node); len(missing) > 0 { unhostable = append(unhostable, mcat.Unhostable{Module: a, Missing: missing}) continue } for _, o := range m.Offers() { satisfied[o] = true } assignedHere[a] = true } because[a] = "assigned" queue = append(queue, a) } // What has already been complained about. A requirement can be wanted by several modules at // once, and saying the same thing twice makes a person hunt for the difference between two // identical lines before realising there is none. reported := map[string]bool{} // **An optional requirement is settled last** (novox/hq ADR 0330). Whether a name is optional is // a question about every module that wants it, and a module pulled in later may want it hard; so // a name only optional modules want so far is put aside until the walk has nothing else, and only // then, if still nobody wants it hard, settled by the optional rule: bound to a provider that is // already assigned or reachable, exactly as a hard one, and otherwise absent — never refused, // and never answered by installing anything. var deferred []string settling := map[string]bool{} absent := map[string]bool{} wantedHard := func(want string) bool { for _, name := range order { if manifestWantsHard(catalogue[name], want) { return true } } return false } wantedOptionally := func(want string) bool { for _, name := range order { if catalogue[name].IsOptional(want) { return true } } return false } leaveAbsent := func(want string) { absent[want] = true reported[want] = true } for { if len(queue) == 0 { if len(deferred) == 0 { break } // What a module chosen since wants hard goes back to the walk, which may install for // it; only when none does is the rest settled, since settling installs nothing and so // can change no other name's answer. var hard, optional []string for _, d := range deferred { if wantedHard(d) { hard = append(hard, d) } else { optional = append(optional, d) } } if len(hard) > 0 { queue, deferred = hard, optional continue } for _, d := range optional { settling[d] = true } queue, deferred = optional, nil } want := queue[0] queue = queue[1:] if chosen[want] || reported[want] { continue } if !settling[want] && because[want] != "assigned" && !wantedHard(want) && wantedOptionally(want) { deferred = append(deferred, want) continue } // Walked as a hard requirement: a refusal names a module that wants it hard, never one that // wanted it only optionally and met it first — whose remedy it is not. if first, known := catalogue[because[want]]; !settling[want] && known && !manifestWantsHard(first, want) { for _, name := range order { if manifestWantsHard(catalogue[name], want) { because[want] = name break } } } // Already answered by something in the set. This is the case that makes assigning zsh do // what a person meant by it. // // **Except when the thing answering it grants a credential** (novox/hq 04-ISSUES/021). A // shell answered here needs nothing more; a database answered here still needs a // password, because the consumer reaches it over TCP from its own container and the // database asks exactly as it would from another machine. **The machine stops being a // trust boundary the moment both ends are containers**, and treating it as one gave the // commonest arrangement of all — a service and its database on one node — the weakest // handling, silently. if satisfied[want] && !isModule(catalogue, want) && !answeredElsewhere(want, node, world, brokered, keeps) { here := func(name string) bool { return chosen[name] || assignedHere[name] } local := providersHere(catalogue, here, want) // Which of them it matters to choose between. A plain capability — a shell, a display // server — asks nothing of whoever answers it, and three shells beside an editor are // not a choice to put to anybody. One that grants a credential, or serves a fact the // consumer cannot guess, becomes a binding, and a binding is to one provider. matter := local if !brokered[want] { matter = nil for _, name := range local { if _, ok := catalogue[name].Serves[want]; ok { matter = append(matter, name) } } } var by mcat.Manifest switch len(matter) { case 0: // Nothing to bind to; satisfied by its presence, as it was. case 1: by = catalogue[matter[0]] default: // Two modules on this machine answer it. Taking whichever a map walk met first // was the rule until novox/hq #258 — random, per plan — and the same stance as // across machines applies: ambiguity is refused, never resolved by picking. // // **Not in the first pass.** That pass exists only to answer *what does this node // offer*, and refusing there makes the machine vanish rather than report a problem // (the sibling case below says why): every other node then loses what this one // provides — the vault, the identity provider — and refuses for a fault that is // this node's to settle. The second pass refuses it properly, where it is asked. if world.Unchecked { continue } c, pinned := world.Pinned[want] if !pinned || c.Node != node.Name || !oneOf(matter, c.Module) { reported[want] = true problems = append(problems, fmt.Sprintf( "%s provides %q %d times, wanted by %s — say which with `pin %s %s %s `: %s", node.Name, want, len(matter), because[want], node.Name, want, node.Name, strings.Join(matter, ", "))) continue } by = catalogue[c.Module] } if brokered[want] { // Answered here, and still a need: the provider is this node. // // **The same loopback fallback as the branch below, and it was missing here.** A // machine with no private network has no `at`, and this branch passed that through // — so a consumer whose file says `${bound::at}:${bound:...:port}` was // handed `:5000`, a name with no host, written into its environment without // complaint. The sibling case a few lines down had the fallback and the reasoning // for it; only this one did not. A machine off the network still reaches itself. at := node.At if at == "" { at = "127.0.0.1" } needs = append(needs, mcat.Needed{ Name: want, From: node.Name, At: at, Module: by.Module, Serves: servedByOne(by, want), For: because[want], SharedOwn: sharedByOne(by, want), CredentialForm: by.CredentialFormOf(want), CredentialTool: by.CredentialToolOf(want), Identity: by.IdentityBoundOf(want)}) } else if served := servedByOne(by, want); len(served) > 0 { // Answered here with no credential to mint, but the provider serves facts the // consumer cannot guess — a port, a model name — and so still needs a binding. // **The reachability rule does not apply**: both ends are on this same machine, so // there is no private network to share and nothing to refuse (novox/hq ADR 0024's // sibling case — a same-node keyless endpoint that the consumer still cannot invent // the port for, exactly what declaration.go's here() was left to patch after the // fact). Delivering it here as a need is what lets knownFor see it, so a file that // says ${bound::port} is filled rather than refused. // // The address is this machine's own name on the private network when it has one, and // loopback when it does not — a machine off the network still reaches itself, and the // consumer's binding must carry a usable `at`. The same fallback declaration.go's // here() applies, done here because this need is now found before here() would run. at := node.At if at == "" { at = "127.0.0.1" } needs = append(needs, mcat.Needed{ Name: want, From: node.Name, At: at, Serves: served, For: because[want], Module: by.Module, CredentialForm: by.CredentialFormOf(want), CredentialTool: by.CredentialToolOf(want)}) } continue } // Answered from elsewhere in the mesh, if it is that kind of name. **Never installed // here.** Choosing a machine to put a database on is a decision with consequences // nobody would want made silently by something resolving a web application. if brokered[want] { reported[want] = true where := world.Offered[want] if settling[want] && len(where) == 0 { // Nothing assigned anywhere answers it: absent, and nothing is put on a machine // for it (ADR 0330 §3), however few modules in the catalogue could. leaveAbsent(want) continue } names := providerNames(where) take := func(p mcat.Provider) { if node.At != "" && p.At == "" || node.At == "" && p.At != "" || node.At == "" && p.At == "" { // One of them is not on the private network, so there is no path between // them. Said here rather than discovered as a connection timing out on a // machine that the mesh reported as configured. problems = append(problems, fmt.Sprintf( "%s needs %q from %s, and they are not both on the private network — "+ "assign %s to whichever is missing it", node.Name, want, p.Node, meshNetwork)) return } shared, form, tool := "", "", "" // A provider whose definition is not in hand is held to the tightest bound the // mesh knows, not to none: what it keeps is not known here (ADR 0225). bound := mcat.DefaultIdentityBound if pm, known := catalogue[p.Module]; known { shared, _ = pm.SharedCredentialOf(want) form, tool = pm.CredentialFormOf(want), pm.CredentialToolOf(want) bound = pm.IdentityBoundOf(want) } needs = append(needs, mcat.Needed{Name: want, From: p.Node, At: p.At, Module: p.Module, Serves: p.Serves, For: because[want], SharedOwn: shared, CredentialForm: form, CredentialTool: tool, Identity: bound, LeftOut: p.LeftOut}) } switch { case world.Unchecked: // First pass. Whether anything answers this is exactly the question this pass // exists to make answerable, so it is not asked here. case len(where) == 0: remedy := "and nothing in the catalogue could" if answers := offers[want]; len(answers) == 1 { remedy = "— assign " + answers[0] + " to a node" } else if len(answers) > 1 { remedy = "— assign one of these to a node: " + strings.Join(answers, ", ") } problems = append(problems, fmt.Sprintf( "nothing in this mesh provides %q, wanted by %s %s", want, because[want], remedy)) case len(where) == 1: if c, pinned := world.Pinned[want]; pinned && !chosenMatches(c, where[0]) { // One provider, and it is not the one this machine was told to use. Silently // using the other would be the mesh overruling a choice somebody made. problems = append(problems, fmt.Sprintf( "%s was told to get %q from %s, and only %s provides it", node.Name, want, c, nameOf(where[0]))) break } take(where[0]) default: c, pinned := world.Pinned[want] if !pinned { // **The seat's holder answers, when a seat delivers this** (novox/hq ADR 0110). // Not a guess, which ADR 0009 refuses: the choice was made once, mesh-wide, by // assigning the holder, where a pin makes it again on every consumer's node. A // pin still wins — it is a consumer coupled to one provider's contents, and has // said so. if holder, held := HolderAmong(want, where, world.Held); held { take(holder) break } problems = append(problems, fmt.Sprintf( "%d providers of %q, wanted by %s — say which with `pin %s %s `: %s", len(where), want, because[want], node.Name, want, strings.Join(names, ", "))) break } matching := chosenAmong(c, where) switch len(matching) { case 0: // Pointed at a provider that does not answer this. Refused rather than // falling back to another: a fallback would quietly move somebody's data to // a machine they did not choose, which is the whole reason this is asked. problems = append(problems, fmt.Sprintf( "%s was told to get %q from %s, and %s does not provide it — these do: %s", node.Name, want, c, c, strings.Join(names, ", "))) case 1: take(matching[0]) default: // A record naming only the node, from before a pin named the module, and that // node answers twice. This once took the last one listed (novox/hq #258): a // coin flip, handed to whoever reads the certificate it chose. problems = append(problems, fmt.Sprintf( "%s was told to get %q from %s, and %s provides it %d times — say which with "+ "`pin %s %s %s `: %s", node.Name, want, c, c.Node, len(matching), node.Name, want, c.Node, strings.Join(providerNames(matching), ", "))) } } continue } // Usable only on its provider's own machine, and not here: refused, never answered by // installing a provider (novox/hq ADR 0208 §3). The display server is the machine's own role, // gated by its graphical session; one pulled in for a window manager is the misassignment // research 026 found. Another node's provider never counts — node scope is never brokered. if machineReach[want] && !isModule(catalogue, want) { reported[want] = true if settling[want] { leaveAbsent(want) continue } if world.Unchecked { // The first pass's refusals take a machine off the network; the second says it. continue } problems = append(problems, fmt.Sprintf( "%q is wanted by %s and is usable only on the machine that provides it, and nothing "+ "assigned to %s does — %s", want, because[want], node.Name, machineReachRemedy(catalogue, want, node.Name))) continue } candidates := offers[want] if settling[want] && !(len(candidates) == 0 && len(world.Licences[want]) > 0) { // Not answered by anything in this node's set, and only installing could answer it // here — which is never done for an optional requirement (ADR 0330 §3), not even // for the only candidate. A licence record still answers it, in the post-pass. leaveAbsent(want) continue } switch len(candidates) { case 0: if len(world.Licences[want]) > 0 { // Answered by a record rather than by a module, and the post-pass below settles // which one. Left alone here: the two questions a refusal must answer — *is // there anything* and *which one* — have different remedies, and answering the // first wrongly would send somebody looking for a module to install. reported[want] = true continue } if world.Unchecked { // The first pass, which exists only to answer *what does this node offer*. // // **Refusing here makes the machine vanish** rather than reporting a problem: the // caller takes a failed resolution to mean it learned nothing about this node, so // one unanswerable requirement on one machine removes that machine from the // private network as far as every OTHER machine is concerned — and they are then // told, wrongly, that the two of them share no network. // // That is a wrong answer about a machine nobody asked about, caused by a fault on // a third. The second pass refuses it properly, where the question is being asked. reported[want] = true continue } reported[want] = true problems = append(problems, fmt.Sprintf( "nothing provides %q, wanted by %s", want, because[want])) continue case 1: // No choice to make, so none is made. This is the case that lets `install i3` bring // in xorg without anybody being asked anything. default: reported[want] = true problems = append(problems, fmt.Sprintf( "%q is wanted by %s and %d modules provide it — choose one and assign it: %s", want, because[want], len(candidates), strings.Join(candidates, ", "))) continue } m := catalogue[candidates[0]] if chosen[m.Module] { continue } chosen[m.Module] = true order = append(order, m.Module) for _, o := range m.Offers() { satisfied[o] = true } if _, ok := because[m.Module]; !ok { because[m.Module] = requiredByWords + because[want] } for _, r := range m.Wants() { if _, ok := because[r]; !ok { because[r] = m.Module } queue = append(queue, r) } } // One need per module that wants it, rather than one per name (novox/hq 04-ISSUES/022). // // **A consumer is a module on a machine, not a machine.** The walk above is a work-list over // names, so a requirement three modules share is visited once and produced one need, carrying // whichever module happened to mention it first. Everything downstream inherited that: one // credential, named after a node, and the other two consumers given nothing at all — a // service that resolves cleanly and then cannot authenticate, which is the exact shape of // 021. // // The record pass below already gets this right and says so. It is the same rule. needs = perConsumer(needs, order, catalogue) // **A consumer bound to its data stays bound to it** (novox/hq ADR 0232). Per consumer, after // the fan-out, because a binding is a consumer's: the walk above chose one provider per name for // the whole machine, and two consumers of it may have been bound at different times. var kept []mcat.KeptBinding if !world.Unchecked { here := func(name string) bool { return chosen[name] || assignedHere[name] } var stuck []string needs, kept, stuck = keepBound(needs, catalogue, node, world, keeps, here) problems = append(problems, stuck...) } // What is answered by a record rather than by a machine. // // A post-pass, deliberately: nothing about it depends on the order requirements were walked // in, and putting it in the queue would mean the reachability rule — which must not apply // here — sitting one branch away from a case it would be wrong for. // // **Refused when the consumer has not said which.** ADR 0024 warns this will be felt: a mesh // holding three ways to reach a model refuses every consumer that has not chosen, which is // correct and is a great deal of saying-which the first time. So the refusal names the // candidates and the exact command, because being right is not the same as being usable. for _, name := range order { m := catalogue[name] for _, want := range m.Wants() { offered, byRecord := world.Licences[want] if !byRecord || len(offered) == 0 { continue } if satisfied[want] { // Something in this node's own set answers it -- a model the mesh runs itself, // most obviously. A record is not consulted when there is a local answer. continue } using, said := world.Using[m.Module][want] if !said { if world.Unchecked { continue } names := make([]string, 0, len(offered)) for _, r := range offered { names = append(names, r.Name) } sort.Strings(names) problems = append(problems, fmt.Sprintf( "%s on %s needs %q and has not been told which one to use — say which with "+ "`licence use %s %s`: %s", m.Module, node.Name, want, node.Name, m.Module, strings.Join(names, ", "))) continue } needs = append(needs, mcat.Needed{ Name: want, From: using.Name, Serves: using.Serves, ByRecord: true, // The module that required it, not whatever first mentioned the name: the key is // sealed per consumer, and a consumer here is a module on a machine. For: m.Module, }) } } resolution := mcat.Resolution{Node: node.Name, At: node.At, PublicDomain: node.PublicDomain, Account: node.Account, AccountHome: node.AccountHome, AgentAccount: node.AgentAccount, AgentAccountHome: node.AgentAccountHome, Capabilities: node.Capabilities, Because: because, Needs: needs, Unhostable: unhostable, Kept: kept, Absent: absentOf(absent, order, catalogue)} for _, n := range providersFirst(order, catalogue) { resolution.Modules = append(resolution.Modules, catalogue[n]) } problems = append(problems, checkCapabilities(resolution.Modules, node)...) claims, claimProblems := checkClaims(resolution.Modules, node, elsewhere, world.Holdings) problems = append(problems, claimProblems...) problems = append(problems, checkResources(resolution.Modules)...) resolution.Claims = claims // Judged over the closure — what this node will actually run — so a holder pulled in by a // requirement counts, and the holders' mutual dependence resolves (novox/hq ADR 0207 §3). // Reported until the switch; refused after it, though never in the first pass, whose refusals // make a machine vanish from the network rather than report anything. resolution.Unheld = UnheldDependencies(catalogue, node.Name, resolution.Modules, nil) // Only a dependency some module in the catalogue could meet is refused: one with no possible // holder has no remedy to name, and stays a report in `status` (novox/hq ADR 0207 §4, read with // the assign rule above it). if enforceSeatDependencies && !world.Unchecked { for _, u := range resolution.Unheld { if len(u.Holders) > 0 { problems = append(problems, u.String()) } } } if len(problems) > 0 { sort.Strings(problems) return mcat.Resolution{}, &Refusal{Problems: problems} } return resolution, nil } // isModule reports whether a name is a module in its own right rather than only something // modules provide. // // A requirement naming a module is not satisfied by something else providing that name: `i3` // requires `xorg` and means xorg, not "anything calling itself a display server". func isModule(catalogue map[string]mcat.Manifest, want string) bool { _, ok := catalogue[want] return ok } // missingCapabilities is the capabilities a module declares that this machine does not have. // // Only the absent ones: a capability the machine reports having is nothing to say. Empty means the // machine can host the module as far as its capabilities go. func missingCapabilities(m mcat.Manifest, node mcat.Node) []string { var missing []string for _, c := range append(append([]string(nil), m.Capabilities...), impliedCapabilities(m)...) { if !node.Capabilities[c] { missing = append(missing, c) } } return missing } // WrongMachine is why a module cannot run here, said as a fact about the machine. // // Said the same whether a module was refused because something running here requires it or reported // as un-applied because a person assigned it directly: the reason is identical, and nothing can be // installed to change it. func WrongMachine(module, capability, node string) string { return fmt.Sprintf( "%s needs the capability %q and %s does not have it — this is the wrong machine, "+ "not a missing module", module, capability, node) } // checkCapabilities refuses a module the machine cannot run that something running here requires. // // A directly-assigned module the machine cannot host is left out of the closure before this runs // and reported as un-applied instead (see Resolve); what reaches here is a module still in the // closure because something requires it, and that set is genuinely incoherent — the requiring // module cannot have its requirement met on this machine. func checkCapabilities(modules []mcat.Manifest, node mcat.Node) []string { var problems []string for _, m := range modules { for _, c := range missingCapabilities(m, node) { problems = append(problems, WrongMachine(m.Module, c, node.Name)) } } return problems } // checkClaims refuses two modules holding one singular thing. // // Within this node's own set, and against what is already held elsewhere for the wider scopes. A // claim at mesh scope is the same idea as the mesh's one hub, said once instead of hard-coded. func checkClaims(modules []mcat.Manifest, node mcat.Node, elsewhere []mcat.Held, holdings []mcat.Held) ([]mcat.Held, []string) { var problems []string var held []mcat.Held // onRecord is every recorded holder of a seat, if a handover ever named one: one for most seats, // and as many as were added for a replicated one (novox/hq ADR 0223). onRecord := func(claim, scope string) []mcat.Held { var out []mcat.Held for _, h := range holdings { hs, ok := mcat.SeatNamed(h.Claim) cs, cok := mcat.SeatNamed(claim) if ok && cok && hs.Name == cs.Name && h.Scope == scope { out = append(out, h) } } return out } // recordedHere says this node's module is one of a seat's holders on record. recordedHere := func(claim, scope, module string) bool { for _, rec := range onRecord(claim, scope) { if rec.Node == node.Name && rec.Module == module { return true } } return false } byScope := map[string]map[string]string{} // scope → claim → module for _, m := range modules { for _, c := range m.Claims { scope := c.At() // **A recorded holder settles it before any counting.** An assignment that could hold // the seat but is not the one on record is eligible, and that is all: it is not a second // holder, so it is not refused, and it does not hold (novox/hq ADR 0131). This is what // lets the next holder stand beside the current one until the seat is handed over. if recs := onRecord(c.Name, scope); len(recs) > 0 { // **A seat held once is on record once** (novox/hq ADR 0223). Only a replicated seat // may have several holders on record; several for any other seat is a store that // disagrees with the mesh's definition of the role, and it is refused, named, rather // than letting two machines answer for what the mesh has one of. if s, known := mcat.SeatNamed(c.Name); known && !s.Replicated && len(recs) > 1 { problems = append(problems, fmt.Sprintf( "%q is on record as held by %d assignments, and it is held once per %s — "+ "`seat %s --to /` records one", c.Name, len(recs), scope, c.Name)) continue } if !recordedHere(c.Name, scope, m.Module) { continue } } if byScope[scope] == nil { byScope[scope] = map[string]string{} } // **One seat under either of its names** (novox/hq ADR 0122): a manifest registered before // a rename claims the former name, and one written after it the current — two claimants of // one seat, compared by the seat they resolve to and not by how each spelled it. seat := heldKey(c.Name, c.Kind) if other, taken := byScope[scope][seat]; taken { problems = append(problems, fmt.Sprintf( "%s and %s both claim %q, and only one thing may hold it per %s", other, m.Module, seat, scope)) continue } byScope[scope][seat] = m.Module held = append(held, mcat.Held{Claim: c.Name, Scope: scope, Node: node.Name, Module: m.Module, Site: node.Site, Kind: c.Kind}) } } // And against the rest of the mesh, for the scopes that reach past this machine. for _, h := range held { for _, e := range elsewhere { if e.Node == node.Name || heldKey(e.Claim, e.Kind) != heldKey(h.Claim, h.Kind) || e.Scope != h.Scope { continue } switch h.Scope { case mcat.ScopeMesh: // **A holder on record is never a second claimant** (novox/hq ADR 0223). Records are // the mesh's settled answer: one for most seats, several only for a replicated seat, // each added by an act. Two holders here are two records, and both hold. if recordedHere(h.Claim, h.Scope, h.Module) { continue } // Both claim and nobody is on record, or this refusal could not have happened. // The remedy is the handover that records the holder (novox/hq ADR 0131, // 04-ISSUES/170), so it is named here rather than left to be found. problems = append(problems, fmt.Sprintf( "%s on %s claims %q, which %s on %s already holds — one per mesh. Nothing is "+ "on record for it; `seat %s --to %s/%s` records the holder, and the other "+ "assignment then stands beside it, eligible and silent", h.Module, node.Name, h.Claim, e.Module, e.Node, h.Claim, e.Node, e.Module)) case mcat.ScopeSite: if node.Site != "" && node.Site == e.Site { problems = append(problems, fmt.Sprintf( "%s on %s claims %q, which %s on %s already holds at %s — one per site", h.Module, node.Name, h.Claim, e.Module, e.Node, node.Site)) } } } } return held, problems } // canonicalSeat is the seat a claimed name refers to, by its current name: itself for a name the mesh // does not know (a module's own seat), its seat's name for a former one. func canonicalSeat(name string) string { if s, known := mcat.SeatNamed(name); known { return s.Name } return name } // checkResources refuses two modules writing the same thing. // // This costs no manifest field: the mesh already holds every resource of every module, so two // declaring one path or one unit are visible without either having to know about the other. A // declared claim is only for the abstract conflicts nothing in the resources reveals. // // **The refusal is about ownership, not use** (novox/hq ADR 0051, 04-ISSUES/036). Two modules // owning one path is the fault this catches — the class of collision this repository keeps // recording. Two modules *accessing* one operator-owned path is not a collision: it is the whole // point of a media stack, where the library server, the managers and the download client must see // the same directories. An access is not a resource and never enters the owner map, so co-access // resolves with no refusal. What is refused is the contradiction — a path one module owns and // another merely accesses — because shared data is the operator's and nobody's to own. func checkResources(modules []mcat.Manifest) []string { // Judged as the machine composes them: where the node-resolver holder writes the resolver file, the // uplink holder's rendering of it is not composed, so it is not a second owner (novox/hq ADR 0247). modules = steppedBack(modules) // And only a claimant of a seat naming a path declares it — the controller's generated resources // included, which are judged here at composition (novox/hq ADR 0287). problems := seatPathsOnMachine(modules) owner := map[string]string{} ownedPath := map[string]string{} // path → owning module, for the access check below packagePresent, packageAbsent := map[string][]string{}, map[string][]string{} for _, m := range modules { // Compared placed, not as written (novox/hq ADR 0112): ${dir:state} is the same six // characters in every module and a different directory in each — two modules' templates // being spelled alike is not two modules owning one path. The default root serves the // comparison: a collision is within one node, and any one root keeps distinct modules' // places distinct. A reference that cannot be placed is left as written — naming what // does not exist is the manifest's own problem, refused where it was made. dirs := dirsFor(m, mcat.Rendering{}) for _, r := range m.Resources { if fmt.Sprint(r["type"]) == "user" { // **An account is shared; what it is set to is not.** Several modules may need one // login: the shell's module sets its shell, the container runtime's puts it in the // `docker` group. The host only ever adds groups, and takes back only a group it put the // account in, when no declared resource still asks for it (novox/hq ADR 0252) — so groups // from several modules cannot contradict each other and are not owned. A shell or a home // is one value, and two modules setting it would each be undone by the other's // apply: each stays one module's per node, and two are refused naming both. name, _ := r["name"].(string) for _, field := range []string{"shell", "home"} { if v, ok := r[field].(string); !ok || v == "" || name == "" { continue } key := "user " + field + " " + name if other, taken := owner[key]; taken && other != m.Module { problems = append(problems, fmt.Sprintf( "%s and %s both set the %s of the user %q", other, m.Module, field, name)) } owner[key] = m.Module } continue } if fmt.Sprint(r["type"]) == "package" { // **A package may be declared by several modules** (novox/hq ADR 0303): declaring it says // it is needed, and the host installs a missing one, never removes one a declaration // stopped naming, so a second declaration changes nothing on the machine. What two // modules can contradict is presence: one declaring it present and another absent // (ADR 0180) would have the host install and remove it in turn, so that pair is refused. if name, _ := r["package"].(string); name != "" { if r["absent"] == true { packageAbsent[name] = append(packageAbsent[name], m.Module) } else { packagePresent[name] = append(packagePresent[name], m.Module) } } continue } for _, field := range []string{"path", "unit", "name"} { value, ok := r[field].(string) if !ok || value == "" { if field == "path" && fmt.Sprint(r["type"]) == "directory" { // A pathless directory owns its placed path — a module stating that // very path is exactly the collision this exists to catch. value = dirs[fmt.Sprint(r["id"])] } if value == "" { continue } } if field == "path" { if placed, err := dirFill(value, dirs, m.Module); err == nil { value = placed } } key := field + " " + value if other, taken := owner[key]; taken && other != m.Module { problems = append(problems, fmt.Sprintf( "%s and %s both declare the %s %q", other, m.Module, field, value)) } owner[key] = m.Module if field == "path" { ownedPath[value] = m.Module } } } // **A file the mesh renders for a module is that module's path too** (novox/hq ADR 0223). The // machine's resolver file is a fact the uplink's holder asks for, and a second module asking // for it, or declaring it, would have the host write one path twice in every apply, the last // one winning. A fact under the operator's home is placed per account and compared nowhere. for _, name := range sortedFacts(m.Facts) { fact := m.Facts[name] if fact.Home || !strings.HasPrefix(fact.Path, "/") { continue } key := "path " + fact.Path if other, taken := owner[key]; taken && other != m.Module { problems = append(problems, fmt.Sprintf( "%s and %s both declare the path %q", other, m.Module, fact.Path)) } owner[key] = m.Module ownedPath[fact.Path] = m.Module } } for _, name := range packageNames(packageAbsent) { if wanted := packagePresent[name]; len(wanted) > 0 { problems = append(problems, fmt.Sprintf("%s declares the package %q absent and %s declare it present (novox/hq ADR 0303)", strings.Join(packageAbsent[name], ", "), name, strings.Join(wanted, ", "))) } } // An accessed path is the operator's, so no module may declare it as one of its own // (novox/hq ADR 0051). Refused here rather than silently tolerated: an owner would create and // chown the very directory another module was told to expect the operator to provide, and the // two intentions cannot both hold. Two modules *accessing* it, by contrast, is never checked, // which is what lets the stack in 04-ISSUES/036 co-resolve. for _, m := range modules { for _, a := range m.Accesses { if a.Path == "" { continue // placed by the assignment; nothing to compare at registration } switch other := ownedPath[a.Path]; other { case "": // Nobody owns it — the ordinary, correct case for shared data. case m.Module: problems = append(problems, fmt.Sprintf( "%s both owns and accesses %q — it is one or the other", m.Module, a.Path)) default: problems = append(problems, fmt.Sprintf( "%s accesses %q, which %s declares it owns — shared data is the operator's, "+ "owned by no module (novox/hq ADR 0051)", m.Module, a.Path, other)) } } } return problems } // meshNetwork is what to assign to a machine that needs to reach another one. // // A string here rather than an import, because the private network is a module the control plane // ships and this package must not depend on the thing it resolves. The name being wrong would // show up as a refusal naming a module nobody can assign, which a test checks // (provided_test.go). The private network's own module since novox/hq ADR 0226 retired the // `networking` bundle that required it. const meshNetwork = "mesh-wireguard" // providersFirst orders a node's modules so that what answers a requirement comes before what // asked for it. // // **The host does not sort** (novox/hq ADR 0005), so the order written here is the order a machine // applies. Selection walks outward from what was assigned, which puts a consumer before the thing // it pulled in — and a module whose service reads a file another module writes then starts before // the file exists. // // It fails, and the next reconcile fixes it. That is the worst shape a fault can take: what gets // remembered is that it works, and nobody looks again // ([04-ISSUES/013](novox/hq)). This is the same fault one level up from where that was found. // // **Stable where nothing requires anything.** Modules with no relation keep the order selection // gave them, because that order is meaningful — assigned first, then what they pulled in — and a // sort that reshuffled unrelated modules would make every declaration's diff unreadable. func providersFirst(order []string, shelf map[string]mcat.Manifest) []string { // Who offers what, among the modules actually chosen. offeredBy := map[string]string{} for _, name := range order { for _, offered := range shelf[name].Offers() { if _, taken := offeredBy[offered]; !taken { offeredBy[offered] = name } } } needs := map[string][]string{} for _, name := range order { for _, want := range shelf[name].Wants() { provider, known := offeredBy[want] if !known || provider == name { continue } needs[name] = append(needs[name], provider) } } var out []string placed := map[string]bool{} var place func(string, map[string]bool) place = func(name string, visiting map[string]bool) { if placed[name] { return } if visiting[name] { // Two modules requiring each other. Left in the order selection gave them rather // than refused: a cycle here is not a machine that cannot work — both are applied, // and one of them starts before the other is ready and settles on the next pass. // Refusing would make a pair of modules that cooperate impossible to assign. return } visiting[name] = true for _, provider := range needs[name] { place(provider, visiting) } delete(visiting, name) placed[name] = true out = append(out, name) } for _, name := range order { place(name, map[string]bool{}) } return out } // perConsumer turns one need per provision into one need per module that wants it. // // Order follows the resolved modules rather than a map, so the same set always produces the same // needs — a declaration whose contents move for no reason makes every push look like a change. // // A need nothing in the set wants is kept as it is rather than dropped. That should not happen; // if it does, the honest outcome is an extra credential nobody reads, not a consumer silently // losing the one it depends on. func perConsumer(needs []mcat.Needed, order []string, catalogue map[string]mcat.Manifest) []mcat.Needed { out := make([]mcat.Needed, 0, len(needs)) // Once per (provision, consumer, local name, provider). The walk over names visits a // same-node provision once per module that mentions it, so two consumers of one produced two // raw needs and, fanned out below, four — the same credential twice for each. Harmless // downstream, since a pair is one row however often it is asked for, and wrong all the same. seen := map[[4]string]bool{} keep := func(n mcat.Needed) { key := [4]string{n.Name, n.For, n.Local, n.From} if seen[key] { return } seen[key] = true out = append(out, n) } for _, n := range needs { var wanted bool for _, name := range order { m, known := catalogue[name] if !known { continue } for _, want := range m.Wants() { if want != n.Name { continue } copied := n copied.For = m.Module // And once per file THIS module keeps the credential in, where it keeps several // (ADR 0094) — here, where the consumer is finally known, not on the walk above. for _, one := range eachLocal(nil, catalogue, copied) { keep(one) } wanted = true break } } if !wanted { keep(n) } } return out } // eachLocal appends the need once per file the wanting module keeps the credential in: once, with // no local name, in the ordinary case; once per local name where the module wants several values // from one provider (ADR 0094). Each is its own pair credential downstream. func eachLocal(needs []mcat.Needed, catalogue map[string]mcat.Manifest, n mcat.Needed) []mcat.Needed { files := catalogue[n.For].SecretFiles(n.Name) // One file under a local name is still a local name: the review found a module keeping ONE // named secret given a need with no local, and so no file, while everything reported success. if len(files) == 0 || (len(files) == 1 && files[0].Local == "") { return append(needs, n) } for _, f := range files { one := n one.Local = f.Local needs = append(needs, one) } return needs } // machineReachRemedy names what would meet a requirement with the machine's reach: every module in // the catalogue that provides it, each with the node seats it holds — for a display, the holders of // node-display-server (novox/hq ADR 0208 §3), named by the seat because that is the role being // asked for, without this code knowing which seat any provision belongs to. func machineReachRemedy(catalogue map[string]mcat.Manifest, want, node string) string { var named []string for _, name := range sortedKeys(catalogue) { m := catalogue[name] provides := false for _, o := range m.Provides { if o.Name == want { provides = true } } if !provides { continue } var held []string for _, c := range m.Claims { if c.At() == mcat.ScopeNode { held = append(held, c.Name) } } if len(held) > 0 { named = append(named, fmt.Sprintf("%s (holds %s)", name, strings.Join(held, ", "))) } else { named = append(named, name) } } if len(named) == 0 { return "and nothing in the catalogue provides it" } return fmt.Sprintf("assign one to %s: %s", node, strings.Join(named, "; ")) } // answeredElsewhere says that a mesh-wide provision this machine could answer itself is answered by // another machine all the same: one this machine was pinned to, or the holder of the mesh seat that // delivers it (novox/hq ADR 0110, ADR 0194). // // **A provider on the machine was taken before either was asked.** The branch for a provision // answered here bound the consumer to the local provider and consulted a pin only between two local // ones, and the seat's holder never; both were read only for a provider on another machine. So when // every machine ran a provider of `wildcard-resolution` — each machine's own resolver, still assigned // while the mesh moved to one — every machine bound its resolver configuration to itself, with the // mesh's one resolver recorded, held and pinned (novox/hq issue 258). The seat is the mesh's choice of // who answers, made once; a provider that merely runs here does not overrule it, and neither does it // overrule a pin somebody set on this machine. // // Not in the first pass, which asks only what this machine offers and has no providers to read. // // **And never by the seat alone for a provision that keeps its consumers' data** (novox/hq ADR // 0232). The rule above was written for the mesh's resolver, which any provider answers alike. Read // for the store's seat, it re-bound every database consumer on a machine running its own store to // the seat's holder on another machine, which made each of them a fresh, empty database there and // left their data behind (novox/hq issue 273). A provider beside a consumer of its data is where that // data is; only a pin — a person — answers it from elsewhere. func answeredElsewhere(want string, node mcat.Node, world mcat.World, brokered, keeps map[string]bool) bool { if world.Unchecked || !brokered[want] { return false } if c, pinned := world.Pinned[want]; pinned { return c.Node != node.Name } if keeps[want] { return false } // **A machine holding the seat answers itself** (novox/hq ADR 0223). With a replicated seat // another machine holds it too, and the first holder in the providers' order may be that one; a // holder is still where this machine's own requirement is answered, so its resolver file lists // itself first. if seat, delivered := mcat.SeatDelivering(want); delivered { for _, h := range append(append([]mcat.Held(nil), world.Holdings...), world.Held...) { if hs, ok := mcat.SeatNamed(h.Claim); ok && hs.Name == seat.Name && h.Scope == seat.Scope && h.Node == node.Name { return false } } } holder, held := HolderAmong(want, world.Offered[want], world.Held) return held && holder.Node != node.Name } // packageNames are the packages a map names, in order. func packageNames(m map[string][]string) []string { names := make([]string, 0, len(m)) for n := range m { names = append(names, n) } sort.Strings(names) return names } // impliedCapabilities are the capabilities a module needs for what its resources say, beyond those it declares: a // container that names groups needs an engine that reads them, so the module is left out of a machine whose report // lacks the capability, naming it, and the field is sent to no engine that would refuse it. None for a module whose // containers name no groups. func impliedCapabilities(m mcat.Manifest) []string { for _, r := range m.Resources { if fmt.Sprint(r["type"]) == "container" && len(groupsOf(r)) > 0 { return []string{mcat.ContainerGroups} } } return nil }