Where the answer to a requirement is allowed to live
Two different things were both written `requires`. A shell, a display
server and a private network have to be on the machine that needs them.
A database does not — it runs somewhere and is reached over the network.
Both were answered the same way, so requiring a database installed
PostgreSQL on every machine that ran a web application.
What a module provides now carries a scope, the same idea claims already
use, written short in the ordinary case:
"provides": ["shell"]
"provides": [{"name": "database", "scope": "mesh"}]
A mesh-scoped requirement is answered by finding the node already running
it — never by installing it here. Choosing a machine to put a database on
is a decision with consequences, and nothing resolving a web application
should make it silently. With nothing anywhere it refuses and says which
module to assign; with two it refuses and says how to choose.
Choosing is `pin <node> <provision> <from>`, kept per node because that
is the granularity the choice has. A pin at a machine that does not
provide it refuses rather than falling back — a fallback would quietly
move somebody's data. One provider does not overrule a pin either.
Resolving a node now needs to know what the others offer, and working
that out needs them resolved, so it is two passes: the first answers only
what each node offers, the second answers everything. Nothing is ever
declared from the first.
A node's plan says what it takes from elsewhere. It is the only part of a
set that stops working when a different machine goes away, and nothing
else in that output would have said so. It is also where a credential
will hang once there is a mechanism for handing one back.
One test found passing for the wrong reason: it read pins through a join
on the provider, which hides a dangling row whether or not it was cleaned
up. It counts rows now, and bites when the cascade is removed.
This commit is contained in:
@@ -24,6 +24,30 @@ type Node struct {
|
||||
Capabilities map[string]bool
|
||||
}
|
||||
|
||||
// World is what the rest of the mesh already has.
|
||||
//
|
||||
// Some requirements cannot be answered on the machine that has them — a database runs somewhere
|
||||
// and is reached over the network — so resolving one node needs to know what the others offer.
|
||||
type World struct {
|
||||
// Held is the claims already taken, for the scopes wider than one node.
|
||||
Held []Held
|
||||
// Offered is what other nodes provide at mesh scope: the name, and which nodes provide it.
|
||||
Offered map[string][]string
|
||||
// Pinned is which node this machine was told to get a provision from, by name. Only consulted
|
||||
// when more than one node could answer -- a choice recorded before it was needed should not
|
||||
// start meaning something the day a second provider appears, and one recorded and then made
|
||||
// unnecessary should not quietly stop applying either.
|
||||
Pinned map[string]string
|
||||
// Unchecked takes brokered requirements on trust instead of refusing when nothing answers
|
||||
// them.
|
||||
//
|
||||
// For the first of two passes. Working out what a node offers the mesh needs that node
|
||||
// resolved, and resolving it may need what the mesh offers — so the first pass answers only
|
||||
// *what does each node offer*, and the second pass answers everything with that in hand. A
|
||||
// declaration is never built from an unchecked resolution.
|
||||
Unchecked bool
|
||||
}
|
||||
|
||||
// Held is a claim somebody already has, used for the scopes wider than one node.
|
||||
type Held struct {
|
||||
Claim string
|
||||
@@ -44,6 +68,20 @@ type Resolution struct {
|
||||
Because map[string]string
|
||||
// Claims is what this node's set holds, so wider scopes can be checked against it.
|
||||
Claims []Held
|
||||
// Needs is what this node's set gets from other nodes. Recorded rather than resolved away,
|
||||
// because it is where a credential will have to be handed back once there is a mechanism for
|
||||
// that, and because "what does this machine depend on that is not on it" has no other answer.
|
||||
Needs []Needed
|
||||
}
|
||||
|
||||
// Needed is one thing this node's set takes from elsewhere in the mesh.
|
||||
type Needed struct {
|
||||
// Name is the provision, as required.
|
||||
Name string
|
||||
// From is the node providing it.
|
||||
From string
|
||||
// For is the module that wanted it.
|
||||
For string
|
||||
}
|
||||
|
||||
// Refusal is why a set of assignments cannot become a declaration.
|
||||
@@ -64,9 +102,33 @@ var ErrAmbiguous = errors.New("more than one module provides that")
|
||||
// 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]Manifest, assigned []string, node Node, elsewhere []Held) (Resolution, error) {
|
||||
func Resolve(catalogue map[string]Manifest, assigned []string, node Node, world World) (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{}
|
||||
for _, m := range catalogue {
|
||||
for _, o := range m.Provides {
|
||||
if o.At() == ScopeMesh {
|
||||
brokered[o.Name] = true
|
||||
continue
|
||||
}
|
||||
local[o.Name] = true
|
||||
}
|
||||
}
|
||||
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{}
|
||||
@@ -82,6 +144,7 @@ func Resolve(catalogue map[string]Manifest, assigned []string, node Node, elsewh
|
||||
chosen := map[string]bool{}
|
||||
because := map[string]string{}
|
||||
var order []string
|
||||
var needs []Needed
|
||||
|
||||
// What the set already offers, which is the first thing a requirement is checked against.
|
||||
//
|
||||
@@ -120,6 +183,67 @@ func Resolve(catalogue map[string]Manifest, assigned []string, node Node, elsewh
|
||||
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]
|
||||
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 chosenNode, pinned := world.Pinned[want]; pinned && chosenNode != 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, chosenNode, where[0]))
|
||||
break
|
||||
}
|
||||
needs = append(needs, Needed{Name: want, From: where[0], For: because[want]})
|
||||
default:
|
||||
sorted := append([]string{}, where...)
|
||||
sort.Strings(sorted)
|
||||
chosenNode, pinned := world.Pinned[want]
|
||||
if !pinned {
|
||||
problems = append(problems, fmt.Sprintf(
|
||||
"%d nodes provide %q, wanted by %s — say which with `pin %s %s <node>`: %s",
|
||||
len(where), want, because[want], node.Name, want,
|
||||
strings.Join(sorted, ", ")))
|
||||
break
|
||||
}
|
||||
var offers bool
|
||||
for _, w := range where {
|
||||
if w == chosenNode {
|
||||
offers = true
|
||||
}
|
||||
}
|
||||
if !offers {
|
||||
// Pointed at a machine 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, chosenNode, chosenNode, strings.Join(sorted, ", ")))
|
||||
break
|
||||
}
|
||||
needs = append(needs, Needed{Name: want, From: chosenNode, For: because[want]})
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
candidates := offers[want]
|
||||
switch len(candidates) {
|
||||
case 0:
|
||||
@@ -159,7 +283,7 @@ func Resolve(catalogue map[string]Manifest, assigned []string, node Node, elsewh
|
||||
}
|
||||
}
|
||||
|
||||
resolution := Resolution{Node: node.Name, Because: because}
|
||||
resolution := Resolution{Node: node.Name, Because: because, Needs: needs}
|
||||
for _, n := range order {
|
||||
resolution.Modules = append(resolution.Modules, catalogue[n])
|
||||
}
|
||||
@@ -437,3 +561,21 @@ func sortedKeys[V any](m map[string]V) []string {
|
||||
sort.Strings(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// Offers is a list of node-scoped provisions, which is what nearly everything is.
|
||||
func Offers(names ...string) []Offer {
|
||||
out := make([]Offer, 0, len(names))
|
||||
for _, n := range names {
|
||||
out = append(out, Offer{Name: n})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// FromAnywhere is a provision answered by whichever node in the mesh runs it.
|
||||
func FromAnywhere(names ...string) []Offer {
|
||||
out := make([]Offer, 0, len(names))
|
||||
for _, n := range names {
|
||||
out = append(out, Offer{Name: n, Scope: ScopeMesh})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user