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:
+153
-24
@@ -63,6 +63,10 @@ func run() error {
|
||||
defer stop()
|
||||
|
||||
switch args[0] {
|
||||
case "pin":
|
||||
return pinCommand(ctx, args[1:], true)
|
||||
case "unpin":
|
||||
return pinCommand(ctx, args[1:], false)
|
||||
case "migrate":
|
||||
return migrate(ctx)
|
||||
case "node":
|
||||
@@ -127,6 +131,8 @@ func usage() {
|
||||
settings set <module> <file> what a module's config should say, for the whole mesh
|
||||
settings set <module> <file> --node <n> ...or for one machine
|
||||
settings clear <module> [--node <n>] take a layer away
|
||||
pin <node> <provision> <from> which node this one gets a provision from
|
||||
unpin <node> <provision> put that question back
|
||||
plan <node> [--files|--json] what that node would run, and why
|
||||
push [<node>] send a node everything it should be
|
||||
version what this binary is
|
||||
@@ -850,7 +856,7 @@ func moduleCommand(ctx context.Context, args []string) error {
|
||||
fmt.Printf(" from %s", short(*commit))
|
||||
}
|
||||
if len(m.Provides) > 0 {
|
||||
fmt.Printf(", providing %s", strings.Join(m.Provides, ", "))
|
||||
fmt.Printf(", providing %s", describeOffers(m.Provides))
|
||||
}
|
||||
fmt.Println()
|
||||
for _, c := range m.Claims {
|
||||
@@ -876,7 +882,7 @@ func moduleCommand(ctx context.Context, args []string) error {
|
||||
m := shelf[n]
|
||||
fmt.Printf("%-20s", m.Module)
|
||||
if len(m.Provides) > 0 {
|
||||
fmt.Printf(" provides %s", strings.Join(m.Provides, ", "))
|
||||
fmt.Printf(" provides %s", describeOffers(m.Provides))
|
||||
}
|
||||
for _, c := range m.Claims {
|
||||
fmt.Printf(" claims %s/%s", c.At(), c.Name)
|
||||
@@ -979,31 +985,17 @@ func planFor(ctx context.Context, inv *inventory.Inventory, nodeName string) (ca
|
||||
}
|
||||
}
|
||||
|
||||
// What every other node already holds, so the claims wider than one machine can be checked.
|
||||
// Resolved rather than read from a table: a claim is held by whatever a node actually runs,
|
||||
// and a record of it would be a second answer that could disagree with the first.
|
||||
var elsewhere []catalogue.Held
|
||||
for _, p := range places {
|
||||
if p.Name == nodeName {
|
||||
continue
|
||||
}
|
||||
theirs, err := inv.Assigned(ctx, p.Name)
|
||||
if err != nil || len(theirs) == 0 {
|
||||
continue
|
||||
}
|
||||
theirCaps, _ := inv.ProfileOf(ctx, p.Name)
|
||||
got, err := catalogue.Resolve(shelf, theirs,
|
||||
catalogue.Node{Name: p.Name, Site: p.Site, Capabilities: theirCaps}, nil)
|
||||
if err != nil {
|
||||
// Their set does not resolve either. Not this node's problem to report, and their
|
||||
// claims cannot be counted because nothing of theirs is running.
|
||||
continue
|
||||
}
|
||||
elsewhere = append(elsewhere, got.Claims...)
|
||||
world, err := theRestOfTheMesh(ctx, inv, shelf, nodeName)
|
||||
if err != nil {
|
||||
return catalogue.Resolution{}, nil, err
|
||||
}
|
||||
world.Pinned, err = inv.PinsFor(ctx, nodeName)
|
||||
if err != nil {
|
||||
return catalogue.Resolution{}, nil, err
|
||||
}
|
||||
|
||||
resolved, err := catalogue.Resolve(shelf, assigned,
|
||||
catalogue.Node{Name: nodeName, Site: site, Capabilities: capabilities}, elsewhere)
|
||||
catalogue.Node{Name: nodeName, Site: site, Capabilities: capabilities}, world)
|
||||
if err != nil {
|
||||
return catalogue.Resolution{}, nil, err
|
||||
}
|
||||
@@ -1033,6 +1025,82 @@ func planFor(ctx context.Context, inv *inventory.Inventory, nodeName string) (ca
|
||||
return resolved, settings, nil
|
||||
}
|
||||
|
||||
// theRestOfTheMesh is what every other node holds and offers.
|
||||
//
|
||||
// Two things at once because they come from the same place — resolving the other nodes — and
|
||||
// because both are facts about what is actually running rather than records that could disagree
|
||||
// with it. A claim is held by whatever a node runs; a provision is offered by whatever a node
|
||||
// runs; neither is a table somebody keeps up to date.
|
||||
//
|
||||
// **Two passes over the others.** What a node offers the mesh needs that node resolved, and
|
||||
// resolving it may need what the mesh offers. So the first pass takes brokered requirements on
|
||||
// trust and answers only *what does each node offer*; the second answers everything with that in
|
||||
// hand. Nothing is ever declared from the first.
|
||||
func theRestOfTheMesh(ctx context.Context, inv *inventory.Inventory,
|
||||
shelf map[string]catalogue.Manifest, exclude string) (catalogue.World, error) {
|
||||
|
||||
// Every node, not only the placed ones. A machine that was never put on the private network
|
||||
// still runs modules, still holds claims, and still offers whatever it offers.
|
||||
nodes, err := inv.Nodes(ctx)
|
||||
if err != nil {
|
||||
return catalogue.World{}, err
|
||||
}
|
||||
places, err := inv.Overlays(ctx)
|
||||
if err != nil {
|
||||
return catalogue.World{}, err
|
||||
}
|
||||
siteOf := map[string]string{}
|
||||
for _, p := range places {
|
||||
siteOf[p.Name] = p.Site
|
||||
}
|
||||
|
||||
type candidate struct {
|
||||
node catalogue.Node
|
||||
assigned []string
|
||||
}
|
||||
var others []candidate
|
||||
for _, n := range nodes {
|
||||
if n.Name == exclude {
|
||||
continue
|
||||
}
|
||||
theirs, err := inv.Assigned(ctx, n.Name)
|
||||
if err != nil || len(theirs) == 0 {
|
||||
continue
|
||||
}
|
||||
caps, _ := inv.ProfileOf(ctx, n.Name)
|
||||
others = append(others, candidate{
|
||||
catalogue.Node{Name: n.Name, Site: siteOf[n.Name], Capabilities: caps}, theirs})
|
||||
}
|
||||
|
||||
offered := map[string][]string{}
|
||||
for _, o := range others {
|
||||
got, err := catalogue.Resolve(shelf, o.assigned, o.node, catalogue.World{Unchecked: true})
|
||||
if err != nil {
|
||||
// Their set does not resolve for some other reason. Not this node's problem to
|
||||
// report, and nothing of theirs is running, so it offers nothing.
|
||||
continue
|
||||
}
|
||||
for _, m := range got.Modules {
|
||||
for _, name := range m.OffersAt(catalogue.ScopeMesh) {
|
||||
offered[name] = append(offered[name], o.node.Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
for k := range offered {
|
||||
sort.Strings(offered[k])
|
||||
}
|
||||
|
||||
world := catalogue.World{Offered: offered}
|
||||
for _, o := range others {
|
||||
got, err := catalogue.Resolve(shelf, o.assigned, o.node, world)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
world.Held = append(world.Held, got.Claims...)
|
||||
}
|
||||
return world, nil
|
||||
}
|
||||
|
||||
func planCommand(ctx context.Context, args []string) error {
|
||||
set := flag.NewFlagSet("plan", flag.ContinueOnError)
|
||||
// Because "one resource" does not tell you whether the settings landed. Being able to read
|
||||
@@ -1090,6 +1158,12 @@ func planCommand(ctx context.Context, args []string) error {
|
||||
for _, c := range plan.Claims {
|
||||
fmt.Printf(" holds %s, one per %s\n", c.Claim, c.Scope)
|
||||
}
|
||||
// What this machine depends on that is not on it. Worth saying out loud: it is the only part
|
||||
// of a node's set that stops working when a *different* machine goes away, and nothing else
|
||||
// in this output would have told anybody that.
|
||||
for _, n := range plan.Needs {
|
||||
fmt.Printf(" needs %s from %s, for %s\n", n.Name, n.From, n.For)
|
||||
}
|
||||
gens, err := generators(ctx, inv)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -1350,3 +1424,58 @@ func settingsCommand(ctx context.Context, args []string) error {
|
||||
return fmt.Errorf("settings has no %q; it has set and clear", args[0])
|
||||
}
|
||||
}
|
||||
|
||||
// describeOffers says what a module provides, and marks the ones answered from anywhere in the
|
||||
// mesh — because "provides a database" and "provides a shell" are read the same way and mean
|
||||
// entirely different things about where the answer has to be.
|
||||
func describeOffers(offers []catalogue.Offer) string {
|
||||
var out []string
|
||||
for _, o := range offers {
|
||||
if o.At() == catalogue.ScopeMesh {
|
||||
out = append(out, o.Name+" (from anywhere in the mesh)")
|
||||
continue
|
||||
}
|
||||
out = append(out, o.Name)
|
||||
}
|
||||
return strings.Join(out, ", ")
|
||||
}
|
||||
|
||||
// pinCommand says which node a machine gets a provision from.
|
||||
//
|
||||
// Needed only when more than one could answer, and recordable before that -- a mesh with one
|
||||
// database should not change where an existing machine gets its data the day a second one
|
||||
// arrives.
|
||||
func pinCommand(ctx context.Context, args []string, setting bool) error {
|
||||
if setting && len(args) != 3 {
|
||||
return errors.New("pin <node> <provision> <from-node>")
|
||||
}
|
||||
if !setting && len(args) != 2 {
|
||||
return errors.New("unpin <node> <provision>")
|
||||
}
|
||||
inv, err := openInventory(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer inv.Close()
|
||||
|
||||
if !setting {
|
||||
if err := inv.UnpinProvision(ctx, args[0], args[1]); err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Printf("%s is no longer told where to get %s from\n", args[0], args[1])
|
||||
return nil
|
||||
}
|
||||
if args[0] == args[2] {
|
||||
// Allowed by nothing here, and worth saying rather than resolving into a confusing
|
||||
// refusal later: a node providing something to itself is a node-scoped provision, and
|
||||
// this field is for the other kind.
|
||||
return fmt.Errorf("%s cannot get %s from itself; that would be a provision this machine "+
|
||||
"provides, which does not need saying", args[0], args[1])
|
||||
}
|
||||
if err := inv.PinProvision(ctx, args[0], args[1], args[2]); err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Printf("%s gets %s from %s\n", args[0], args[1], args[2])
|
||||
fmt.Printf(" run `push %s` to send it\n", args[0])
|
||||
return nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user