A pin names the module as well as the node; a node that answers twice is refused
A provider is a (node, module) pair (design 23), and the pin — the one way a consumer names its provider — named only the node. Two modules on one node can both answer a provision (public-acme and step-ca both offer acme-ca on novox), and then the resolver, given a pin naming that node, took the last provider listed: a coin flip. The same ambiguity beside the consumer was settled by a map walk — random per plan — which is how novox's own route-proxy got its issuer (novox/hq #258). - `pin <node> <provision> <from-node> <module>`: both halves, always. The console gains `pin` and `unpin`. The provider may be on the consumer's own node, since two modules beside it can both answer. - The resolver refuses ambiguity instead of picking, across machines and beside the consumer alike, naming every candidate as node/module and the form of the pin that settles it. A plain capability that grants nothing and serves nothing (three shells beside an editor) is not a choice to put to anybody and stays as it was. - provision_pin gains a nullable module (0050); records made before are completed where the node they name answers once, and left for a person where it answers twice (0051). - The provider of something already satisfied is looked for among what was assigned, not only what the walk has reached — a consumer reached before the provider beside it no longer loses its binding. - The start-time check that every declared verb is runnable samples each verb's required arguments from its schema instead of three guessed keys. Live consequence: a node that has two providers of one bound provision assigned (novox: acme-ca) resolves only once pinned — `pin novox acme-ca novox public-acme`.
This commit is contained in:
@@ -841,24 +841,28 @@ func (i *Inventory) SettingsFor(ctx context.Context, nodeName, module string) ([
|
||||
return layers, rows.Err()
|
||||
}
|
||||
|
||||
// PinProvision records which node a machine gets a provision from.
|
||||
// PinProvision records which provider a machine gets a provision from: a module, and the node it
|
||||
// runs on — both, always (novox/hq #258). A provision comes from a module, and the same module on
|
||||
// two machines is two answers, so neither half alone says which.
|
||||
//
|
||||
// Only needed when more than one node could answer. Recordable before that, because a mesh with
|
||||
// one database should not change where an existing machine gets its data the day a second
|
||||
// arrives.
|
||||
func (i *Inventory) PinProvision(ctx context.Context, nodeName, provision, provider string) error {
|
||||
// Only needed when more than one could answer. Recordable before that, because a mesh with one
|
||||
// database should not change where an existing machine gets its data the day a second arrives.
|
||||
func (i *Inventory) PinProvision(ctx context.Context, nodeName, provision, providerNode, module string) error {
|
||||
if strings.TrimSpace(module) == "" {
|
||||
return fmt.Errorf("a pin names the module providing %q as well as the node it runs on", provision)
|
||||
}
|
||||
node, err := i.NodeByName(ctx, nodeName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
from, err := i.NodeByName(ctx, provider)
|
||||
from, err := i.NodeByName(ctx, providerNode)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = i.store.Pool().Exec(ctx,
|
||||
`insert into provision_pin (node, name, provider) values ($1, $2, $3)
|
||||
on conflict (node, name) do update set provider = excluded.provider, pinned_at = now()`,
|
||||
node.ID, provision, from.ID)
|
||||
`insert into provision_pin (node, name, provider, module) values ($1, $2, $3, $4)
|
||||
on conflict (node, name) do update set provider = excluded.provider, module = excluded.module, pinned_at = now()`,
|
||||
node.ID, provision, from.ID, module)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -879,27 +883,28 @@ func (i *Inventory) UnpinProvision(ctx context.Context, nodeName, provision stri
|
||||
return nil
|
||||
}
|
||||
|
||||
// PinsFor is what a node was told about where its provisions come from.
|
||||
func (i *Inventory) PinsFor(ctx context.Context, nodeName string) (map[string]string, error) {
|
||||
// PinsFor is what a node was told about where its provisions come from. A record from before a pin
|
||||
// named the module carries the node alone; the resolver honours it while it is unambiguous.
|
||||
func (i *Inventory) PinsFor(ctx context.Context, nodeName string) (map[string]catalogue.Chosen, error) {
|
||||
node, err := i.NodeByName(ctx, nodeName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rows, err := i.store.Pool().Query(ctx,
|
||||
`select p.name, n.name from provision_pin p join node n on n.id = p.provider
|
||||
`select p.name, n.name, coalesce(p.module, '') from provision_pin p join node n on n.id = p.provider
|
||||
where p.node = $1`, node.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
out := map[string]string{}
|
||||
out := map[string]catalogue.Chosen{}
|
||||
for rows.Next() {
|
||||
var name, provider string
|
||||
if err := rows.Scan(&name, &provider); err != nil {
|
||||
var name, provider, module string
|
||||
if err := rows.Scan(&name, &provider, &module); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out[name] = provider
|
||||
out[name] = catalogue.Chosen{Node: provider, Module: module}
|
||||
}
|
||||
return out, rows.Err()
|
||||
}
|
||||
|
||||
@@ -385,19 +385,19 @@ func TestAPinSurvivesAndCanBeChanged(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
if err := inv.PinProvision(ctx, "user", "postgres-database", "first"); err != nil {
|
||||
if err := inv.PinProvision(ctx, "user", "postgres-database", "first", "postgres"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// Changing the answer replaces it rather than adding a second, or a machine would be told to
|
||||
// use two databases and nothing would say which.
|
||||
if err := inv.PinProvision(ctx, "user", "postgres-database", "second"); err != nil {
|
||||
if err := inv.PinProvision(ctx, "user", "postgres-database", "second", "postgres"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
pins, err := inv.PinsFor(ctx, "user")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(pins) != 1 || pins["postgres-database"] != "second" {
|
||||
if len(pins) != 1 || pins["postgres-database"].Node != "second" || pins["postgres-database"].Module != "postgres" {
|
||||
t.Fatalf("got %v", pins)
|
||||
}
|
||||
if err := inv.UnpinProvision(ctx, "user", "postgres-database"); err != nil {
|
||||
@@ -422,7 +422,7 @@ func TestAPinGoesWhenTheProviderLeavesTheMesh(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
if err := inv.PinProvision(ctx, "consumer", "postgres-database", "provider"); err != nil {
|
||||
if err := inv.PinProvision(ctx, "consumer", "postgres-database", "provider", "postgres"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := inv.store.Pool().Exec(ctx, `delete from node where name = 'provider'`); err != nil {
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
-- A pin names the module as well as the node (novox/hq #258).
|
||||
--
|
||||
-- 0008 said "not a module: the same module on two machines is two answers, and which machine is the
|
||||
-- whole question". Half right. Two modules on one machine can both answer a provision — public-acme
|
||||
-- and step-ca both offer acme-ca on novox — and then which *module* is the whole question, and a
|
||||
-- node alone cannot ask it. The resolver, given a node that answered twice, took the last one listed.
|
||||
--
|
||||
-- A provider is a (node, module) pair (design 23), and a pin names the pair. Nullable, so a record
|
||||
-- made before this was asked keeps meaning what it meant: honoured while that node answers once,
|
||||
-- refused with the module asked for when it answers twice.
|
||||
alter table provision_pin add column module text;
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
-- The records already made are completed where the mesh can tell: a pin naming a node on which
|
||||
-- exactly one assigned module offers the provision (or is the module itself, for a requirement that
|
||||
-- names a module) gets that module. A node that answers twice is left to say which — the resolver
|
||||
-- refuses it with the module asked for, rather than this guessing on its behalf.
|
||||
update provision_pin p
|
||||
set module = sub.module
|
||||
from (
|
||||
select p2.node, p2.name, min(a.module) as module, count(distinct a.module) as answers
|
||||
from provision_pin p2
|
||||
join assignment a on a.node = p2.provider
|
||||
join module m on m.name = a.module
|
||||
where p2.module is null
|
||||
and (a.module = p2.name
|
||||
or exists (select 1
|
||||
from jsonb_array_elements(coalesce(m.manifest -> 'provides', '[]'::jsonb)) e
|
||||
where (case when jsonb_typeof(e) = 'string' then e #>> '{}' else e ->> 'name' end) = p2.name))
|
||||
group by p2.node, p2.name
|
||||
) sub
|
||||
where sub.node = p.node and sub.name = p.name and sub.answers = 1;
|
||||
@@ -0,0 +1,86 @@
|
||||
package inventory
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// A pin made before it named the module (migration 0051, novox/hq #258): completed where the node it
|
||||
// names answers once, left for a person where it answers twice.
|
||||
|
||||
func legacyPin(t *testing.T, inv *Inventory, node, provision, provider string) {
|
||||
t.Helper()
|
||||
_, err := inv.store.Pool().Exec(context.Background(),
|
||||
`insert into provision_pin (node, name, provider)
|
||||
select u.id, $2, p.id from node u, node p where u.name = $1 and p.name = $3`,
|
||||
node, provision, provider)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func completeEarlierPins(t *testing.T, inv *Inventory) {
|
||||
t.Helper()
|
||||
sql, err := os.ReadFile("migrations/0051-a-pin-made-before-is-completed.sql")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := inv.store.Pool().Exec(context.Background(), string(sql)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAPinMadeBeforeIsCompletedWhenTheNodeAnswersOnce(t *testing.T) {
|
||||
inv := fresh(t)
|
||||
ctx := context.Background()
|
||||
for _, n := range []string{"user", "provider"} {
|
||||
if _, err := inv.AddNode(ctx, n); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
for _, m := range []string{"postgres", "redis"} {
|
||||
if err := inv.RegisterModule(ctx, manifest(m, []string{m + "-database"}, nil), Source{}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := inv.Assign(ctx, "provider", m); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
legacyPin(t, inv, "user", "postgres-database", "provider")
|
||||
completeEarlierPins(t, inv)
|
||||
pins, err := inv.PinsFor(ctx, "user")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got := pins["postgres-database"]; got.Node != "provider" || got.Module != "postgres" {
|
||||
t.Fatalf("the record was not completed with the one module that answers: %+v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAPinMadeBeforeIsLeftOpenWhenTheNodeAnswersTwice(t *testing.T) {
|
||||
inv := fresh(t)
|
||||
ctx := context.Background()
|
||||
for _, n := range []string{"user", "provider"} {
|
||||
if _, err := inv.AddNode(ctx, n); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
for _, m := range []string{"public-acme", "step-ca"} {
|
||||
if err := inv.RegisterModule(ctx, manifest(m, []string{"acme-ca"}, nil), Source{}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := inv.Assign(ctx, "provider", m); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
legacyPin(t, inv, "user", "acme-ca", "provider")
|
||||
completeEarlierPins(t, inv)
|
||||
pins, err := inv.PinsFor(ctx, "user")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got := pins["acme-ca"]; got.Node != "provider" || got.Module != "" {
|
||||
t.Fatalf("a node that answers twice was guessed for: %+v", got)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user