ADR 0593 §7: the composer with its refusals leaves mesh-sdk, so mesh-sdk carries no composition rule. The catalogue functions only the node-engine's composition reaches (Resolve, Resolution's Compose, Declaration, DataItems, LeftOut, Rules and the rest, the settings, placeholder and template refusals, the rendering helpers) are copied with their tests into internal/compose/resolution; a method of a mesh-sdk shape becomes a function taking it first. The composition calls its own copies, and the captured catalogue its tests read lives here now, shared by mesh-compose's tests. Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
1188 lines
50 KiB
Go
1188 lines
50 KiB
Go
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 <module>`: %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:<provision>: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:<provision>: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 <node> <module>`: %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 <module>`: %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 <name> %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 <node>/<module>` 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
|
|
}
|