Merge pull request 'A mesh seat may be replicated: the resolver held on two machines (hq ADR 0223)' (#65) from feat/the-mesh-has-two-resolvers into main
This commit was merged in pull request #65.
This commit is contained in:
@@ -111,6 +111,14 @@ func TestTheRegistryTrustAndEveryImageFollowThePortTheNodeGaveTheStore(t *testin
|
|||||||
if _, err := assign(ctx, open, "laptop", "app"); err != nil {
|
if _, err := assign(ctx, open, "laptop", "app"); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
// The runtime's trust is the runtime's module's to write (novox/hq ADR 0222): a stand-in for it
|
||||||
|
// asks where this machine reaches the store, as the docker module does.
|
||||||
|
register(t, open, catalogue.Manifest{Module: "runtime", Version: "1",
|
||||||
|
Resources: []map[string]any{{"id": "daemon", "type": "file", "path": "/etc/docker/daemon.json",
|
||||||
|
"into": "json", "content": `{"insecure-registries": ["${seat:mesh-artifact-store:reach}"]}` + "\n"}}})
|
||||||
|
if _, err := assign(ctx, open, "laptop", "runtime"); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
on := map[string]bool{"anchor": true, "laptop": true}
|
on := map[string]bool{"anchor": true, "laptop": true}
|
||||||
node, port, found, err := artifactStoreOnNetwork(ctx, open.inventory, on)
|
node, port, found, err := artifactStoreOnNetwork(ctx, open.inventory, on)
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import (
|
|||||||
"sort"
|
"sort"
|
||||||
|
|
||||||
"github.com/novox/mesh-controller/internal/catalogue"
|
"github.com/novox/mesh-controller/internal/catalogue"
|
||||||
|
"github.com/novox/mesh-controller/internal/inventory"
|
||||||
|
"github.com/novox/mesh-controller/internal/overlay"
|
||||||
)
|
)
|
||||||
|
|
||||||
// recordDerivedHolders writes down who holds each mesh-scoped seat that nobody was ever recorded
|
// recordDerivedHolders writes down who holds each mesh-scoped seat that nobody was ever recorded
|
||||||
@@ -90,3 +92,72 @@ func recordDerivedHolders(ctx context.Context, open *stores) ([]string, error) {
|
|||||||
}
|
}
|
||||||
return said, nil
|
return said, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// replicatedHolders is, for each replicated mesh seat, every machine on the private network holding
|
||||||
|
// it — by internal name, at its private address (novox/hq ADR 0223). What a machine's resolver file
|
||||||
|
// lists for `mesh-dns-resolver`; the rendering puts the machine itself first when it is one.
|
||||||
|
//
|
||||||
|
// **The holders on record, and only the sole claimant when there are none** — the same answer the
|
||||||
|
// resolver gives about who holds (ADR 0131, issue 170). An assignment standing beside the holders,
|
||||||
|
// eligible and silent, is not listed: it becomes a holder by `seat <name> --add`, an act, never by
|
||||||
|
// being assigned. Two claimants with nothing on record are refused at resolution, so neither is
|
||||||
|
// listed here. A holder off the private network is left out: a resolver named at an address nothing
|
||||||
|
// answers is a lookup that waits out its timeout on every name.
|
||||||
|
func replicatedHolders(ctx context.Context, inv *inventory.Inventory,
|
||||||
|
shelf map[string]catalogue.Manifest) (map[string]map[string]string, error) {
|
||||||
|
var replicated []catalogue.Seat
|
||||||
|
for _, s := range catalogue.Seats() {
|
||||||
|
if s.Replicated && s.Scope == catalogue.ScopeMesh {
|
||||||
|
replicated = append(replicated, s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(replicated) == 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
recorded, err := inv.Holdings(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
places, err := onTheNetwork(ctx, inv, shelf)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
address := map[string]string{}
|
||||||
|
for _, p := range places {
|
||||||
|
address[p.Name] = p.Address
|
||||||
|
}
|
||||||
|
entries, err := inv.Catalogued(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
out := map[string]map[string]string{}
|
||||||
|
for _, seat := range replicated {
|
||||||
|
var nodes []string
|
||||||
|
for _, h := range recorded {
|
||||||
|
if hs, ok := catalogue.SeatNamed(h.Claim); ok && hs.Name == seat.Name && h.Scope == seat.Scope {
|
||||||
|
nodes = append(nodes, h.Node)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(nodes) == 0 {
|
||||||
|
var derived []string
|
||||||
|
for _, e := range entries {
|
||||||
|
for _, c := range e.Manifest.Claims {
|
||||||
|
if cs, ok := catalogue.SeatNamed(c.Name); ok && cs.Name == seat.Name && c.At() == seat.Scope {
|
||||||
|
derived = append(derived, e.On...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(derived) == 1 {
|
||||||
|
nodes = derived
|
||||||
|
}
|
||||||
|
}
|
||||||
|
at := map[string]string{}
|
||||||
|
for _, n := range nodes {
|
||||||
|
if address[n] != "" {
|
||||||
|
at[overlay.InternalName(n)] = address[n]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out[seat.Name] = at
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -194,6 +194,7 @@ func usage() {
|
|||||||
seats [--json] every seat this mesh defines, what it delivers, and who holds it
|
seats [--json] every seat this mesh defines, what it delivers, and who holds it
|
||||||
seat rename <from> <to> rename a seat; its former name still resolves (ADR 0122)
|
seat rename <from> <to> rename a seat; its former name still resolves (ADR 0122)
|
||||||
seat <name> --to <node>/<module> hand a seat to that assignment as one act; never empty in between (ADR 0131)
|
seat <name> --to <node>/<module> hand a seat to that assignment as one act; never empty in between (ADR 0131)
|
||||||
|
seat <name> --add <node>/<module> add a holder beside the others, for a replicated seat (ADR 0223)
|
||||||
board [--listen ADDR] the same three questions, as a page that holds nothing
|
board [--listen ADDR] the same three questions, as a page that holds nothing
|
||||||
api --issuer URL [--listen A] assign and unassign over http, for a surface that is not here
|
api --issuer URL [--listen A] assign and unassign over http, for a surface that is not here
|
||||||
assign <node> <module>... put modules on a node, judged together (ADR 0207)
|
assign <node> <module>... put modules on a node, judged together (ADR 0207)
|
||||||
|
|||||||
@@ -707,6 +707,13 @@ func renderingFor(ctx context.Context, open *stores, node string,
|
|||||||
return catalogue.Rendering{}, inventory.Node{}, err
|
return catalogue.Rendering{}, inventory.Node{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// And who holds each replicated seat, where (novox/hq ADR 0223): every machine's resolver file
|
||||||
|
// lists every holder of the mesh's resolver.
|
||||||
|
replicas, err := replicatedHolders(ctx, inv, shelf)
|
||||||
|
if err != nil {
|
||||||
|
return catalogue.Rendering{}, inventory.Node{}, err
|
||||||
|
}
|
||||||
|
|
||||||
// **The bus is never public** (novox/hq ADR 0169). It was a foundation port — widened from the
|
// **The bus is never public** (novox/hq ADR 0169). It was a foundation port — widened from the
|
||||||
// broker's own `from: mesh` to from-anywhere on the broker's host, so a machine could enrol
|
// broker's own `from: mesh` to from-anywhere on the broker's host, so a machine could enrol
|
||||||
// before it had an address on the private network. A machine joins through the tunnel now, and
|
// before it had an address on the private network. A machine joins through the tunnel now, and
|
||||||
@@ -783,7 +790,7 @@ func renderingFor(ctx context.Context, open *stores, node string,
|
|||||||
BusMembership: memberships[node],
|
BusMembership: memberships[node],
|
||||||
Settings: settings, Generators: gens, Grants: grants, Needed: needed, Ports: ports,
|
Settings: settings, Generators: gens, Grants: grants, Needed: needed, Ports: ports,
|
||||||
Certificate: certificate, Authority: authority, Mesh: private, Names: names,
|
Certificate: certificate, Authority: authority, Mesh: private, Names: names,
|
||||||
Machines: machines, Zones: zones,
|
Machines: machines, Zones: zones, Holders: replicas,
|
||||||
Suffix: overlay.Suffix(), MeshRange: meshRange, TunnelInterface: overlay.Interface, Accounts: accounts, Foundation: foundation,
|
Suffix: overlay.Suffix(), MeshRange: meshRange, TunnelInterface: overlay.Interface, Accounts: accounts, Foundation: foundation,
|
||||||
Kept: kept, Adopted: record.Adopted, OutwardLinks: outwardLinks,
|
Kept: kept, Adopted: record.Adopted, OutwardLinks: outwardLinks,
|
||||||
Given: given, Taken: taken, Seats: seats, ArtifactStore: artifactStore, SeatReach: reach, Built: built,
|
Given: given, Taken: taken, Seats: seats, ArtifactStore: artifactStore, SeatReach: reach, Built: built,
|
||||||
|
|||||||
@@ -29,11 +29,13 @@ type seatHolder struct {
|
|||||||
|
|
||||||
// seatRow is one seat and who holds it. Unheld is an answer — "this mesh has no X" — not a fault.
|
// seatRow is one seat and who holds it. Unheld is an answer — "this mesh has no X" — not a fault.
|
||||||
type seatRow struct {
|
type seatRow struct {
|
||||||
Seat string `json:"seat"`
|
Seat string `json:"seat"`
|
||||||
Scope string `json:"scope"`
|
Scope string `json:"scope"`
|
||||||
Delivers string `json:"delivers,omitempty"`
|
Delivers string `json:"delivers,omitempty"`
|
||||||
Decision string `json:"decision"`
|
Decision string `json:"decision"`
|
||||||
Holders []seatHolder `json:"holders"`
|
// Replicated says the seat may be held on several machines at once (novox/hq ADR 0223).
|
||||||
|
Replicated bool `json:"replicated,omitempty"`
|
||||||
|
Holders []seatHolder `json:"holders"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// seatsHeld is every seat the mesh defines with its holders, and every claim held that names no
|
// seatsHeld is every seat the mesh defines with its holders, and every claim held that names no
|
||||||
@@ -47,7 +49,7 @@ func seatsHeld(seats []catalogue.Seat, held []catalogue.Held) ([]seatRow, []cata
|
|||||||
rows := make([]seatRow, 0, len(seats))
|
rows := make([]seatRow, 0, len(seats))
|
||||||
for _, s := range seats {
|
for _, s := range seats {
|
||||||
row := seatRow{Seat: s.Name, Scope: s.Scope, Delivers: s.Delivers, Decision: s.Decision,
|
row := seatRow{Seat: s.Name, Scope: s.Scope, Delivers: s.Delivers, Decision: s.Decision,
|
||||||
Holders: []seatHolder{}}
|
Replicated: s.Replicated, Holders: []seatHolder{}}
|
||||||
seen := map[seatHolder]bool{}
|
seen := map[seatHolder]bool{}
|
||||||
for _, h := range held {
|
for _, h := range held {
|
||||||
// Resolve the held claim to a seat rather than comparing names, so a record naming a
|
// Resolve the held claim to a seat rather than comparing names, so a record naming a
|
||||||
@@ -104,9 +106,13 @@ func seatCommand(ctx context.Context, args []string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if len(args) == 3 && args[1] == "--to" {
|
if len(args) == 3 && args[1] == "--to" {
|
||||||
return handOver(ctx, args[0], args[2])
|
return handOver(ctx, args[0], args[2], false)
|
||||||
}
|
}
|
||||||
return fmt.Errorf("seat rename <from> <to> | seat <name> --to <node>/<module>")
|
if len(args) == 3 && args[1] == "--add" {
|
||||||
|
return handOver(ctx, args[0], args[2], true)
|
||||||
|
}
|
||||||
|
return fmt.Errorf("seat rename <from> <to> | seat <name> --to <node>/<module> | " +
|
||||||
|
"seat <name> --add <node>/<module>")
|
||||||
}
|
}
|
||||||
|
|
||||||
// handOver makes one assignment the holder of a seat, as one act, so the seat is never without a
|
// handOver makes one assignment the holder of a seat, as one act, so the seat is never without a
|
||||||
@@ -119,7 +125,12 @@ func seatCommand(ctx context.Context, args []string) error {
|
|||||||
// **not** checked is whether the module is running yet: that is what `push` confirms afterwards,
|
// **not** checked is whether the module is running yet: that is what `push` confirms afterwards,
|
||||||
// and refusing to record a handover to a module the node has not started would make the handover
|
// and refusing to record a handover to a module the node has not started would make the handover
|
||||||
// impossible to do before the switch instead of as the switch.
|
// impossible to do before the switch instead of as the switch.
|
||||||
func handOver(ctx context.Context, seatName, to string) error {
|
//
|
||||||
|
// **Or adds one holder beside the others, for a replicated seat** (novox/hq ADR 0223): `--add`
|
||||||
|
// records the named assignment as a further holder and leaves every holder on record as it is. A
|
||||||
|
// seat held once refuses it, naming `--to`; `--to` on a replicated seat replaces every holder with
|
||||||
|
// the one named, as it always did.
|
||||||
|
func handOver(ctx context.Context, seatName, to string, adding bool) error {
|
||||||
nodeName, module, ok := strings.Cut(to, "/")
|
nodeName, module, ok := strings.Cut(to, "/")
|
||||||
if !ok || nodeName == "" || module == "" {
|
if !ok || nodeName == "" || module == "" {
|
||||||
return fmt.Errorf("the new holder is named <node>/<module>, not %q", to)
|
return fmt.Errorf("the new holder is named <node>/<module>, not %q", to)
|
||||||
@@ -135,6 +146,10 @@ func handOver(ctx context.Context, seatName, to string) error {
|
|||||||
if !known {
|
if !known {
|
||||||
return fmt.Errorf("%q is not a seat this mesh defines — `seats` lists them", seatName)
|
return fmt.Errorf("%q is not a seat this mesh defines — `seats` lists them", seatName)
|
||||||
}
|
}
|
||||||
|
if adding && !seat.Replicated {
|
||||||
|
return fmt.Errorf("%s is held once per %s, so a second holder cannot be added beside the first — "+
|
||||||
|
"`seat %s --to %s` hands it over", seat.Name, seat.Scope, seat.Name, to)
|
||||||
|
}
|
||||||
assigned, err := inv.Assigned(ctx, nodeName)
|
assigned, err := inv.Assigned(ctx, nodeName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -157,6 +172,7 @@ func handOver(ctx context.Context, seatName, to string) error {
|
|||||||
return fmt.Errorf("%s is assigned but not in the catalogue, which should not happen", module)
|
return fmt.Errorf("%s is assigned but not in the catalogue, which should not happen", module)
|
||||||
}
|
}
|
||||||
var was string
|
var was string
|
||||||
|
var held []string
|
||||||
holdings, err := inv.Holdings(ctx)
|
holdings, err := inv.Holdings(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -164,6 +180,7 @@ func handOver(ctx context.Context, seatName, to string) error {
|
|||||||
for _, h := range holdings {
|
for _, h := range holdings {
|
||||||
if hs, ok := catalogue.SeatNamed(h.Claim); ok && hs.Name == seat.Name {
|
if hs, ok := catalogue.SeatNamed(h.Claim); ok && hs.Name == seat.Name {
|
||||||
was = h.Node
|
was = h.Node
|
||||||
|
held = append(held, h.Node)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -187,6 +204,15 @@ func handOver(ctx context.Context, seatName, to string) error {
|
|||||||
} else if err := catalogue.CanHold(*m, seat); err != nil {
|
} else if err := catalogue.CanHold(*m, seat); err != nil {
|
||||||
return fmt.Errorf("%s cannot hold %s: %w", module, seat.Name, err)
|
return fmt.Errorf("%s cannot hold %s: %w", module, seat.Name, err)
|
||||||
}
|
}
|
||||||
|
if adding {
|
||||||
|
if err := inv.AddSeatHolder(ctx, seat.Name, seat.Scope, nodeName, module); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
fmt.Printf("%s is held by %s on %s, beside what was on record: %s\n", seat.Name, module, nodeName,
|
||||||
|
strings.Join(held, ", "))
|
||||||
|
fmt.Printf(" `push --behind` re-declares every machine that reads the seat's holders\n")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
if err := inv.HoldSeat(ctx, seat.Name, seat.Scope, nodeName, module); err != nil {
|
if err := inv.HoldSeat(ctx, seat.Name, seat.Scope, nodeName, module); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -159,6 +159,11 @@ type Rendering struct {
|
|||||||
// 0199): the mesh's resolver forwards each one there.
|
// 0199): the mesh's resolver forwards each one there.
|
||||||
Zones []ZoneAt
|
Zones []ZoneAt
|
||||||
|
|
||||||
|
// Holders is, for each replicated mesh seat, every machine holding it, by internal name and
|
||||||
|
// private address (novox/hq ADR 0223) — the same shape as Machines. What a machine's resolver
|
||||||
|
// file lists: every holder of the mesh's resolver, this machine first if it is one.
|
||||||
|
Holders map[string]map[string]string
|
||||||
|
|
||||||
Settings SettingsBy
|
Settings SettingsBy
|
||||||
Generators map[string]Generator
|
Generators map[string]Generator
|
||||||
// Grants are the credentials this node must create, for the provisions it offers. Passed in
|
// Grants are the credentials this node must create, for the provisions it offers. Passed in
|
||||||
@@ -980,7 +985,7 @@ func (r Resolution) compose(with Rendering, owner map[string]string,
|
|||||||
// plane's; making a name resolve is the module's software. Emitted as ordinary files under
|
// plane's; making a name resolve is the module's software. Emitted as ordinary files under
|
||||||
// this module's name, so they are applied, reported and removed exactly as anything else
|
// this module's name, so they are applied, reported and removed exactly as anything else
|
||||||
// it declares.
|
// it declares.
|
||||||
given, err := FactsWithZonesInto(m, r, with.Names, with.Machines, with.Accounts, with.Suffix, with.Zones)
|
given, err := FactsFrom(m, r, with)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -767,16 +767,27 @@ func checkClaims(modules []Manifest, node Node, elsewhere []Held, holdings []Hel
|
|||||||
var problems []string
|
var problems []string
|
||||||
var held []Held
|
var held []Held
|
||||||
|
|
||||||
// onRecord is the recorded holder of a seat, if a handover ever named one.
|
// onRecord is every recorded holder of a seat, if a handover ever named one: one for most seats,
|
||||||
onRecord := func(claim, scope string) (Held, bool) {
|
// and as many as were added for a replicated one (novox/hq ADR 0223).
|
||||||
|
onRecord := func(claim, scope string) []Held {
|
||||||
|
var out []Held
|
||||||
for _, h := range holdings {
|
for _, h := range holdings {
|
||||||
hs, ok := SeatNamed(h.Claim)
|
hs, ok := SeatNamed(h.Claim)
|
||||||
cs, cok := SeatNamed(claim)
|
cs, cok := SeatNamed(claim)
|
||||||
if ok && cok && hs.Name == cs.Name && h.Scope == scope {
|
if ok && cok && hs.Name == cs.Name && h.Scope == scope {
|
||||||
return h, true
|
out = append(out, h)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return Held{}, false
|
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
|
byScope := map[string]map[string]string{} // scope → claim → module
|
||||||
@@ -787,8 +798,18 @@ func checkClaims(modules []Manifest, node Node, elsewhere []Held, holdings []Hel
|
|||||||
// the seat but is not the one on record is eligible, and that is all: it is not a second
|
// 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
|
// 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.
|
// lets the next holder stand beside the current one until the seat is handed over.
|
||||||
if rec, recorded := onRecord(c.Name, scope); recorded {
|
if recs := onRecord(c.Name, scope); len(recs) > 0 {
|
||||||
if rec.Node != node.Name || rec.Module != m.Module {
|
// **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 := 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
|
continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -815,6 +836,12 @@ func checkClaims(modules []Manifest, node Node, elsewhere []Held, holdings []Hel
|
|||||||
}
|
}
|
||||||
switch h.Scope {
|
switch h.Scope {
|
||||||
case ScopeMesh:
|
case 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.
|
// 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,
|
// 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.
|
// 04-ISSUES/170), so it is named here rather than left to be found.
|
||||||
@@ -1156,6 +1183,18 @@ func answeredElsewhere(want string, node Node, world World, brokered map[string]
|
|||||||
if c, pinned := world.Pinned[want]; pinned {
|
if c, pinned := world.Pinned[want]; pinned {
|
||||||
return c.Node != node.Name
|
return c.Node != node.Name
|
||||||
}
|
}
|
||||||
|
// **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 := SeatDelivering(want); delivered {
|
||||||
|
for _, h := range append(append([]Held(nil), world.Holdings...), world.Held...) {
|
||||||
|
if hs, ok := 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)
|
holder, held := HolderAmong(want, world.Offered[want], world.Held)
|
||||||
return held && holder.Node != node.Name
|
return held && holder.Node != node.Name
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -84,25 +84,24 @@ func TestTheResolverForwardsToFixedUpstreamsAndNeverReadsResolvConf(t *testing.T
|
|||||||
t.Errorf("the mesh's resolver still reads or listens on %q", never)
|
t.Errorf("the mesh's resolver still reads or listens on %q", never)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// And the file that decides what the machine asks names the mesh's resolver first, by address,
|
// And the file that decides what the machine asks names every one of the mesh's resolvers, by
|
||||||
// and a public one second, asked only when the first is silent (ADR 0196).
|
// address, from the seat's holders, and no public one (ADR 0223): a resolver library that asks
|
||||||
var resolv string
|
// every listed server at once takes the first reply, and a public "no such name" for a mesh name
|
||||||
for _, r := range catalogueManifest(t, "resolv-conf").Resources {
|
// won it.
|
||||||
if r["path"] == "/etc/resolv.conf" {
|
fact, ok := catalogueManifest(t, "resolv-conf").Facts["resolvers"]
|
||||||
resolv, _ = r["content"].(string)
|
if !ok || fact.Path != "/etc/resolv.conf" {
|
||||||
|
t.Fatalf("the machine's resolver file is not rendered from the roster: %+v", fact)
|
||||||
|
}
|
||||||
|
if !strings.Contains(fact.Template, `{{range index .Holders "mesh-dns-resolver"}}nameserver {{.Address}}`) {
|
||||||
|
t.Errorf("resolv.conf does not list every holder of the mesh's resolver:\n%s", fact.Template)
|
||||||
|
}
|
||||||
|
for _, line := range strings.Split(fact.Template, "\n") {
|
||||||
|
if strings.HasPrefix(line, "nameserver ") && !strings.Contains(line, "{{") {
|
||||||
|
t.Errorf("resolv.conf names a resolver of its own beside the mesh's: %s", line)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
var nameservers []string
|
if !strings.Contains(fact.Template, "options timeout:1 attempts:2 edns0\n") {
|
||||||
for _, line := range strings.Split(resolv, "\n") {
|
t.Errorf("a silent resolver is not passed over after one short wait:\n%s", fact.Template)
|
||||||
if strings.HasPrefix(line, "nameserver ") {
|
|
||||||
nameservers = append(nameservers, strings.TrimPrefix(line, "nameserver "))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if len(nameservers) != 2 || nameservers[0] != "${bound:wildcard-resolution:address}" || nameservers[1] != "1.1.1.1" {
|
|
||||||
t.Errorf("resolv.conf names %v; the mesh's resolver by address first, a public one second", nameservers)
|
|
||||||
}
|
|
||||||
if !strings.Contains(resolv, "\noptions timeout:1 attempts:1") {
|
|
||||||
t.Errorf("the fallback is not reached after one short attempt:\n%s", resolv)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -126,6 +125,7 @@ func TestTheResolverAndWhatAsksItComposeOnOneMachine(t *testing.T) {
|
|||||||
// happen to be the same map, since nothing routed is part of it.
|
// happen to be the same map, since nothing routed is part of it.
|
||||||
Names: twoMachines, Machines: twoMachines, Suffix: "internal",
|
Names: twoMachines, Machines: twoMachines, Suffix: "internal",
|
||||||
Zones: []ZoneAt{{Zone: "incus", Address: "10.42.0.2", Port: 5353}},
|
Zones: []ZoneAt{{Zone: "incus", Address: "10.42.0.2", Port: 5353}},
|
||||||
|
Holders: map[string]map[string]string{"mesh-dns-resolver": {"anchor.internal": "10.42.0.1"}},
|
||||||
Needed: map[string]map[string]string{"dnsmasq": {"broker": "sealed"}},
|
Needed: map[string]map[string]string{"dnsmasq": {"broker": "sealed"}},
|
||||||
Settings: SettingsBy{"dnsmasq": {{From: "the mesh", Values: map[string]any{"listen-addresses": "127.0.0.1"}}}},
|
Settings: SettingsBy{"dnsmasq": {{From: "the mesh", Values: map[string]any{"listen-addresses": "127.0.0.1"}}}},
|
||||||
})
|
})
|
||||||
@@ -205,9 +205,9 @@ func TestTheResolverAndWhatAsksItComposeOnOneMachine(t *testing.T) {
|
|||||||
t.Errorf("the runtime is not reloaded when its file changes, so live-restore never takes effect")
|
t.Errorf("the runtime is not reloaded when its file changes, so live-restore never takes effect")
|
||||||
}
|
}
|
||||||
|
|
||||||
resolv := ids["resolv-conf.resolv"]
|
resolv := ids["resolv-conf.fact-resolvers"]
|
||||||
if resolv == nil || !strings.Contains(resolv["content"].(string), "\nnameserver 10.42.0.1\nnameserver 1.1.1.1\n") {
|
if resolv == nil || !strings.Contains(resolv["content"].(string), "\nnameserver 10.42.0.1\noptions ") {
|
||||||
t.Fatalf("the machine is not pointed at the resolver by address, with the public fallback: %v", resolv)
|
t.Fatalf("the machine is not pointed at the resolver by address, and only at it: %v", resolv)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -64,6 +64,12 @@ type rosterView struct {
|
|||||||
// Zones is every zone a module in the mesh answers itself, with where its answerer is (novox/hq
|
// Zones is every zone a module in the mesh answers itself, with where its answerer is (novox/hq
|
||||||
// ADR 0199) — what the mesh's resolver forwards. Ordered by zone.
|
// ADR 0199) — what the mesh's resolver forwards. Ordered by zone.
|
||||||
Zones []rosterZone
|
Zones []rosterZone
|
||||||
|
// Holders is the machines holding each replicated mesh seat, by seat (novox/hq ADR 0223) — what
|
||||||
|
// a machine's resolver file lists for `mesh-dns-resolver`. **This machine first when it is one
|
||||||
|
// of them**, then the rest by name: the nearest holder is asked first, and two renderings of
|
||||||
|
// one mesh on one machine are one file. A template reads one seat's with
|
||||||
|
// `index .Holders "<seat>"`; a seat nobody holds ranges over nothing.
|
||||||
|
Holders map[string][]rosterEntry
|
||||||
}
|
}
|
||||||
|
|
||||||
// rosterZone is one zone as a template sees it: the zone, and the address and port answering it.
|
// rosterZone is one zone as a template sees it: the zone, and the address and port answering it.
|
||||||
@@ -96,6 +102,13 @@ func FactsInto(m Manifest, r Resolution, every, machines, accounts map[string]st
|
|||||||
// FactsWithZonesInto is FactsInto with the mesh's zones in the view, for a template that ranges them.
|
// FactsWithZonesInto is FactsInto with the mesh's zones in the view, for a template that ranges them.
|
||||||
func FactsWithZonesInto(m Manifest, r Resolution, every, machines, accounts map[string]string, suffix string,
|
func FactsWithZonesInto(m Manifest, r Resolution, every, machines, accounts map[string]string, suffix string,
|
||||||
zones []ZoneAt) ([]map[string]any, error) {
|
zones []ZoneAt) ([]map[string]any, error) {
|
||||||
|
return FactsFrom(m, r, Rendering{Names: every, Machines: machines, Accounts: accounts, Suffix: suffix,
|
||||||
|
Zones: zones})
|
||||||
|
}
|
||||||
|
|
||||||
|
// FactsFrom renders the roster files a module asked for from everything the mesh composed for this
|
||||||
|
// machine: its machines, zones and the holders of each replicated seat.
|
||||||
|
func FactsFrom(m Manifest, r Resolution, with Rendering) ([]map[string]any, error) {
|
||||||
if len(m.Facts) == 0 {
|
if len(m.Facts) == 0 {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
@@ -107,10 +120,11 @@ func FactsWithZonesInto(m Manifest, r Resolution, every, machines, accounts map[
|
|||||||
|
|
||||||
view := rosterView{
|
view := rosterView{
|
||||||
Node: r.Node,
|
Node: r.Node,
|
||||||
Suffix: strings.TrimPrefix(suffixOr(suffix), "."),
|
Suffix: strings.TrimPrefix(suffixOr(with.Suffix), "."),
|
||||||
Names: entriesFrom(every, accounts, suffix),
|
Names: entriesFrom(with.Names, with.Accounts, with.Suffix),
|
||||||
Machines: entriesFrom(machines, accounts, suffix),
|
Machines: entriesFrom(with.Machines, with.Accounts, with.Suffix),
|
||||||
Zones: zonesFrom(zones),
|
Zones: zonesFrom(with.Zones),
|
||||||
|
Holders: holdersFrom(with.Holders, with.Accounts, with.Suffix, r.Node),
|
||||||
}
|
}
|
||||||
|
|
||||||
out := make([]map[string]any, 0, len(names))
|
out := make([]map[string]any, 0, len(names))
|
||||||
@@ -250,3 +264,18 @@ func zonesFrom(zones []ZoneAt) []rosterZone {
|
|||||||
sort.Slice(out, func(i, j int) bool { return out[i].Zone < out[j].Zone })
|
sort.Slice(out, func(i, j int) bool { return out[i].Zone < out[j].Zone })
|
||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// holdersFrom is each replicated seat's holders as a template ranges them: this machine first when
|
||||||
|
// it holds the seat, then the others by name (novox/hq ADR 0223). A machine with no address is left
|
||||||
|
// out, as everywhere in the roster — a resolver named at nothing is a lookup that hangs.
|
||||||
|
func holdersFrom(holders map[string]map[string]string, accounts map[string]string, suffix, node string) map[string][]rosterEntry {
|
||||||
|
out := make(map[string][]rosterEntry, len(holders))
|
||||||
|
for seat, at := range holders {
|
||||||
|
entries := entriesFrom(at, accounts, suffix)
|
||||||
|
sort.SliceStable(entries, func(i, j int) bool {
|
||||||
|
return entries[i].Name == node && entries[j].Name != node
|
||||||
|
})
|
||||||
|
out[seat] = entries
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|||||||
+33
-15
@@ -21,8 +21,16 @@ import (
|
|||||||
type Seat struct {
|
type Seat struct {
|
||||||
// Name is what a manifest claims.
|
// Name is what a manifest claims.
|
||||||
Name string
|
Name string
|
||||||
// Scope is where there may be only one holder.
|
// Scope is where there may be only one holder — unless the seat is Replicated.
|
||||||
Scope string
|
Scope string
|
||||||
|
// Replicated says a mesh seat may be held on several machines at once, each holder answering the
|
||||||
|
// same thing (novox/hq ADR 0223): the mesh's resolver, held on the anchor and the home server so a
|
||||||
|
// machine's resolver file lists two that give one answer. Each holder is on record, added by an
|
||||||
|
// act (`seat <name> --add <node>/<module>`), never by being assigned: two claimants with nothing on
|
||||||
|
// record are refused exactly as for any mesh seat. One per machine still — two modules on one
|
||||||
|
// node claiming it are refused. Compiled, never stored, like Needs: it is the mesh's definition of
|
||||||
|
// the role, and the store's rows carry no column for it.
|
||||||
|
Replicated bool
|
||||||
// Delivers is the provision the seat's holder answers for, or empty. A seat that delivers a
|
// Delivers is the provision the seat's holder answers for, or empty. A seat that delivers a
|
||||||
// provision may only be held by a module providing it at the seat's scope, and its holder is
|
// provision may only be held by a module providing it at the seat's scope, and its holder is
|
||||||
// what a requirement for that provision resolves to when several modules provide it.
|
// what a requirement for that provision resolves to when several modules provide it.
|
||||||
@@ -140,10 +148,14 @@ var defaultSeats = append([]Seat{
|
|||||||
// that machine unresolvable in the meantime. Deleted once no registered manifest claims it.
|
// that machine unresolvable in the meantime. Deleted once no registered manifest claims it.
|
||||||
{Name: "mesh-build-machine", Scope: ScopeMesh,
|
{Name: "mesh-build-machine", Scope: ScopeMesh,
|
||||||
Accepts: []string{"build"}, Emits: []string{"started", "built", "log.*"}, Decision: "novox/hq ADR 0190"},
|
Accepts: []string{"build"}, Emits: []string{"started", "built", "log.*"}, Decision: "novox/hq ADR 0190"},
|
||||||
// **The mesh's one resolver** (novox/hq ADR 0194, 0196): every node's internal domain, held in one
|
// **The mesh's resolvers** (novox/hq ADR 0194, 0196, 0223): every node's internal domain, and
|
||||||
// place, and every node and container asks it first. Delivers what a machine's resolver
|
// every node and container asks them and nothing else. Replicated since ADR 0223: held on more
|
||||||
// configuration requires, so that requirement resolves to the holder wherever it is placed.
|
// than one machine, each answering the same names from the same roster, and every machine's
|
||||||
{Name: "mesh-dns-resolver", Scope: ScopeMesh, Delivers: "wildcard-resolution", Decision: "novox/hq ADR 0194"},
|
// resolver file lists every holder — its own first — and no public resolver, so whichever answers
|
||||||
|
// first gives the one answer. Delivers what a machine's resolver configuration requires, so that
|
||||||
|
// requirement resolves to a holder wherever they are placed.
|
||||||
|
{Name: "mesh-dns-resolver", Scope: ScopeMesh, Delivers: "wildcard-resolution", Replicated: true,
|
||||||
|
Decision: "novox/hq ADR 0194, ADR 0223"},
|
||||||
// **A machine's /etc/hosts is one module's** (novox/hq ADR 0199): its holder writes the machine's
|
// **A machine's /etc/hosts is one module's** (novox/hq ADR 0199): its holder writes the machine's
|
||||||
// own lines and keeps every other line as the operator's, changed through these three verbs on that
|
// own lines and keeps every other line as the operator's, changed through these three verbs on that
|
||||||
// machine alone. The controller holds none of it.
|
// machine alone. The controller holds none of it.
|
||||||
@@ -314,11 +326,12 @@ func UseSeats(s []Seat) {
|
|||||||
row.Accepts, row.Emits, row.Serves = d.Accepts, d.Emits, d.Serves
|
row.Accepts, row.Emits, row.Serves = d.Accepts, d.Emits, d.Serves
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// What a seat receives and what its holder needs are never stored (novox/hq ADR 0212, ADR
|
// What a seat receives, what its holder needs and whether it is replicated are never stored
|
||||||
// 0220), so they are always the compiled ones.
|
// (novox/hq ADR 0212, ADR 0220, ADR 0223), so they are always the compiled ones.
|
||||||
if d, known := byName[row.Name]; known {
|
if d, known := byName[row.Name]; known {
|
||||||
row.Receives = d.Receives
|
row.Receives = d.Receives
|
||||||
row.Needs = d.Needs
|
row.Needs = d.Needs
|
||||||
|
row.Replicated = d.Replicated
|
||||||
}
|
}
|
||||||
merged = append(merged, row)
|
merged = append(merged, row)
|
||||||
}
|
}
|
||||||
@@ -498,19 +511,24 @@ func seatNames() string {
|
|||||||
// two modules on one node could both provide a provision, and only the one holding the seat
|
// two modules on one node could both provide a provision, and only the one holding the seat
|
||||||
// answers for it. Nothing when no seat delivers the provision, when nobody holds
|
// answers for it. Nothing when no seat delivers the provision, when nobody holds
|
||||||
// it, or when the holder is not among the providers offered.
|
// it, or when the holder is not among the providers offered.
|
||||||
|
//
|
||||||
|
// **The first holder in the providers' own order** (novox/hq ADR 0223). A replicated seat has
|
||||||
|
// several, and the answer must not depend on the order the mesh happened to resolve its machines
|
||||||
|
// in: the providers come sorted by machine, so every consumer is bound to the same one. A seat with
|
||||||
|
// one holder gets the same answer as before.
|
||||||
func HolderAmong(provision string, providers []Provider, held []Held) (Provider, bool) {
|
func HolderAmong(provision string, providers []Provider, held []Held) (Provider, bool) {
|
||||||
seat, delivered := SeatDelivering(provision)
|
seat, delivered := SeatDelivering(provision)
|
||||||
if !delivered {
|
if !delivered {
|
||||||
return Provider{}, false
|
return Provider{}, false
|
||||||
}
|
}
|
||||||
for _, h := range held {
|
for _, p := range providers {
|
||||||
// Resolve the held claim to a seat rather than comparing names, so a record naming a seat's
|
for _, h := range held {
|
||||||
// former name still matches it after a rename (novox/hq ADR 0122).
|
// Resolve the held claim to a seat rather than comparing names, so a record naming a
|
||||||
hs, ok := SeatNamed(h.Claim)
|
// seat's former name still matches it after a rename (novox/hq ADR 0122).
|
||||||
if !ok || hs.Name != seat.Name || h.Scope != seat.Scope {
|
hs, ok := SeatNamed(h.Claim)
|
||||||
continue
|
if !ok || hs.Name != seat.Name || h.Scope != seat.Scope {
|
||||||
}
|
continue
|
||||||
for _, p := range providers {
|
}
|
||||||
if p.Node == h.Node && p.Module == h.Module {
|
if p.Node == h.Node && p.Module == h.Module {
|
||||||
return p, true
|
return p, true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,199 @@
|
|||||||
|
package catalogue
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// The mesh has two resolvers (novox/hq ADR 0223): `mesh-dns-resolver` is replicated, held on the
|
||||||
|
// anchor and on the home server, each answering the same names; every machine's resolver file lists
|
||||||
|
// every holder — its own first when it is one — and no public resolver. ADR 0196 listed the mesh's
|
||||||
|
// resolver then a public one, and musl asks both at once and takes the first reply: from the home
|
||||||
|
// server the public "no such name" for the anchor's mesh name won, every time, in every Alpine build.
|
||||||
|
|
||||||
|
// resolverMachines is the anchor and the home server holding the resolver, and a laptop holding nothing.
|
||||||
|
var resolverMachines = map[string]string{
|
||||||
|
"anchor.internal": "10.42.0.1", "laptop.internal": "10.42.0.2", "home.internal": "10.42.0.3"}
|
||||||
|
|
||||||
|
// bothResolvers is the two holders on record, as `seat mesh-dns-resolver --add` leaves them.
|
||||||
|
var bothResolvers = []Held{
|
||||||
|
{Claim: "mesh-dns-resolver", Scope: ScopeMesh, Node: "anchor", Module: "dnsmasq"},
|
||||||
|
{Claim: "mesh-dns-resolver", Scope: ScopeMesh, Node: "home", Module: "dnsmasq"},
|
||||||
|
}
|
||||||
|
|
||||||
|
// twoResolverShelf is the resolver, what asks it, and a stand-in answering `mesh-addressing`.
|
||||||
|
func twoResolverShelf(t *testing.T) map[string]Manifest {
|
||||||
|
t.Helper()
|
||||||
|
return map[string]Manifest{
|
||||||
|
"net": {Module: "net", Version: "1", Provides: []Offer{{Name: "mesh-addressing"}}},
|
||||||
|
"dnsmasq": catalogueManifest(t, "dnsmasq"),
|
||||||
|
"resolv-conf": catalogueManifest(t, "resolv-conf"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// worldWithout is the rest of the mesh as a plan for one machine sees it: every other holder's claim
|
||||||
|
// and offer, and both holders on record.
|
||||||
|
func worldWithout(node string) World {
|
||||||
|
w := World{Holdings: bothResolvers, Offered: map[string][]Provider{}}
|
||||||
|
for _, h := range bothResolvers {
|
||||||
|
if h.Node == node {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
w.Held = append(w.Held, h)
|
||||||
|
w.Offered["wildcard-resolution"] = append(w.Offered["wildcard-resolution"],
|
||||||
|
Provider{Node: h.Node, At: h.Node + ".internal", Module: h.Module})
|
||||||
|
}
|
||||||
|
return w
|
||||||
|
}
|
||||||
|
|
||||||
|
// resolvConfOn resolves and composes one machine and answers with the nameservers its resolver file
|
||||||
|
// lists, in order, and the file.
|
||||||
|
func resolvConfOn(t *testing.T, node string, assigned []string) ([]string, string) {
|
||||||
|
t.Helper()
|
||||||
|
got, err := Resolve(twoResolverShelf(t), assigned, Node{Name: node, At: node + ".internal"}, worldWithout(node))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("%s does not resolve with two resolvers on record: %v", node, err)
|
||||||
|
}
|
||||||
|
out, err := got.Declaration(Rendering{
|
||||||
|
Names: resolverMachines, Machines: resolverMachines, Suffix: "internal",
|
||||||
|
Holders: map[string]map[string]string{"mesh-dns-resolver": {
|
||||||
|
"anchor.internal": "10.42.0.1", "home.internal": "10.42.0.3"}},
|
||||||
|
Needed: map[string]map[string]string{"dnsmasq": {"broker": "sealed"}},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("%s does not compose: %v", node, err)
|
||||||
|
}
|
||||||
|
file := byID(out)["resolv-conf.fact-resolvers"]
|
||||||
|
if file == nil || file["path"] != "/etc/resolv.conf" {
|
||||||
|
t.Fatalf("%s was given no resolver file: %v", node, file)
|
||||||
|
}
|
||||||
|
content, _ := file["content"].(string)
|
||||||
|
var servers []string
|
||||||
|
for _, line := range strings.Split(content, "\n") {
|
||||||
|
if strings.HasPrefix(line, "nameserver ") {
|
||||||
|
servers = append(servers, strings.TrimPrefix(line, "nameserver "))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return servers, content
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTwoResolversComposeOnBothHoldersAndEachListsItselfFirst(t *testing.T) {
|
||||||
|
for node, want := range map[string][]string{
|
||||||
|
"anchor": {"10.42.0.1", "10.42.0.3"},
|
||||||
|
"home": {"10.42.0.3", "10.42.0.1"},
|
||||||
|
} {
|
||||||
|
servers, content := resolvConfOn(t, node, []string{"dnsmasq", "resolv-conf"})
|
||||||
|
if strings.Join(servers, " ") != strings.Join(want, " ") {
|
||||||
|
t.Errorf("%s lists %v; itself first, then the other holder: %v\n%s", node, servers, want, content)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAMachineHoldingNoResolverListsBothAndNoPublicOne(t *testing.T) {
|
||||||
|
servers, content := resolvConfOn(t, "laptop", []string{"resolv-conf"})
|
||||||
|
if strings.Join(servers, " ") != "10.42.0.1 10.42.0.3" {
|
||||||
|
t.Errorf("the laptop lists %v; every holder, by name, and nothing else:\n%s", servers, content)
|
||||||
|
}
|
||||||
|
for _, public := range []string{"1.1.1.1", "8.8.8.8", "9.9.9.9"} {
|
||||||
|
if strings.Contains(content, public) {
|
||||||
|
t.Errorf("a public resolver is listed beside the mesh's (ADR 0223):\n%s", content)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A holder answers its own requirement, even though the other holder sorts first (issue 258 kept).
|
||||||
|
func TestAHolderAnswersItsOwnRequirement(t *testing.T) {
|
||||||
|
got, err := Resolve(twoResolverShelf(t), []string{"dnsmasq", "resolv-conf"},
|
||||||
|
Node{Name: "home", At: "home.internal"}, worldWithout("home"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
for _, n := range got.Needs {
|
||||||
|
if n.Name == "wildcard-resolution" && n.From != "home" {
|
||||||
|
t.Errorf("the home server's resolver configuration is bound to %s; it holds the seat itself", n.From)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A seat held once is still held once: a second claimant on another machine is refused while
|
||||||
|
// nothing is on record, and a store recording two holders of it is refused, naming the seat.
|
||||||
|
func TestASingleHolderMeshSeatStillRefusesASecondHolder(t *testing.T) {
|
||||||
|
store := shelf(mod("postgres", nil, nil, nil, Claim{Name: "mesh-store", Scope: ScopeMesh}))
|
||||||
|
other := Held{Claim: "mesh-store", Scope: ScopeMesh, Node: "anchor", Module: "postgres"}
|
||||||
|
if _, err := Resolve(store, []string{"postgres"}, workstation(), World{Held: []Held{other}}); err == nil ||
|
||||||
|
!strings.Contains(err.Error(), "one per mesh") {
|
||||||
|
t.Errorf("a second claimant of a seat held once was not refused: %v", err)
|
||||||
|
}
|
||||||
|
here := Held{Claim: "mesh-store", Scope: ScopeMesh, Node: workstation().Name, Module: "postgres"}
|
||||||
|
_, err := Resolve(store, []string{"postgres"}, workstation(),
|
||||||
|
World{Held: []Held{other}, Holdings: []Held{other, here}})
|
||||||
|
if err == nil || !strings.Contains(err.Error(), "on record as held by 2") {
|
||||||
|
t.Errorf("two holders on record for a seat held once were not refused: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Replicated is not "whoever is assigned": two claimants with nothing on record are refused, as for
|
||||||
|
// any mesh seat, and each holder is added by an act.
|
||||||
|
func TestTwoUnrecordedClaimantsOfTheReplicatedSeatAreRefused(t *testing.T) {
|
||||||
|
w := worldWithout("home")
|
||||||
|
w.Holdings = nil
|
||||||
|
_, err := Resolve(twoResolverShelf(t), []string{"dnsmasq"}, Node{Name: "home", At: "home.internal"}, w)
|
||||||
|
if err == nil || !strings.Contains(err.Error(), "seat mesh-dns-resolver --to") {
|
||||||
|
t.Errorf("a second resolver with nothing on record was not refused, naming the handover: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only the mesh's resolver is replicated: a seat being replicated is a decision, recorded.
|
||||||
|
func TestOnlyTheResolverIsReplicated(t *testing.T) {
|
||||||
|
for _, s := range Seats() {
|
||||||
|
if s.Replicated != (s.Name == "mesh-dns-resolver") {
|
||||||
|
t.Errorf("%s replicated = %v; only mesh-dns-resolver is (ADR 0223)", s.Name, s.Replicated)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
UseSeats([]Seat{{Name: "mesh-dns-resolver", Scope: ScopeMesh, Delivers: "wildcard-resolution"}})
|
||||||
|
defer UseSeats(DefaultSeats())
|
||||||
|
if s, _ := SeatNamed("mesh-dns-resolver"); !s.Replicated {
|
||||||
|
t.Error("loading the set from the store, which has no column for it, lost the resolver's replication")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Every consumer is bound to the same holder whatever order the mesh resolved its machines in.
|
||||||
|
func TestTheFirstHolderIsTheFirstProvider(t *testing.T) {
|
||||||
|
providers := []Provider{{Node: "anchor", Module: "dnsmasq"}, {Node: "home", Module: "dnsmasq"}}
|
||||||
|
for _, held := range [][]Held{bothResolvers, {bothResolvers[1], bothResolvers[0]}} {
|
||||||
|
if p, ok := HolderAmong("wildcard-resolution", providers, held); !ok || p.Node != "anchor" {
|
||||||
|
t.Errorf("held in order %v answered %v", held, p)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// One host record per machine, beside its wildcard (novox/hq issue 262): a name with a host record
|
||||||
|
// says it exists and has no IPv6 address, where the wildcard alone said there is no such name, and
|
||||||
|
// musl reads that as final.
|
||||||
|
func TestTheResolverHasOneHostRecordPerMachine(t *testing.T) {
|
||||||
|
got, err := Resolve(twoResolverShelf(t), []string{"dnsmasq"}, Node{Name: "anchor", At: "anchor.internal"},
|
||||||
|
World{})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
out, err := got.Declaration(Rendering{Names: resolverMachines, Machines: resolverMachines, Suffix: "internal",
|
||||||
|
Needed: map[string]map[string]string{"dnsmasq": {"broker": "sealed"}}})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
content, _ := byID(out)["dnsmasq.fact-node-zones"]["content"].(string)
|
||||||
|
records := map[string]int{}
|
||||||
|
for _, line := range strings.Split(content, "\n") {
|
||||||
|
if strings.HasPrefix(line, "host-record=") {
|
||||||
|
records[strings.TrimPrefix(line, "host-record=")]++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for name, at := range resolverMachines {
|
||||||
|
if records[name+","+at] != 1 {
|
||||||
|
t.Errorf("%s has %d host records at %s, and has one:\n%s", name, records[name+","+at], at, content)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(records) != len(resolverMachines) {
|
||||||
|
t.Errorf("%d host records for %d machines:\n%s", len(records), len(resolverMachines), content)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -97,3 +97,57 @@ func TestAMachinesMembershipIsOneRowReplacedAndGoesWithTheMachine(t *testing.T)
|
|||||||
t.Fatalf("a re-told membership did not replace the first: %v", got)
|
t.Fatalf("a re-told membership did not replace the first: %v", got)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A replicated seat has several holders on record (novox/hq ADR 0223): each is added beside the
|
||||||
|
// others, adding one twice changes nothing, a handover still leaves exactly one, and unassigning one
|
||||||
|
// takes only its own row.
|
||||||
|
func TestAReplicatedSeatHasSeveralHoldersOnRecord(t *testing.T) {
|
||||||
|
resolver := catalogue.Manifest{Module: "resolver", Version: "1",
|
||||||
|
Provides: []catalogue.Offer{{Name: "wildcard-resolution", Scope: catalogue.ScopeMesh}},
|
||||||
|
Claims: []catalogue.Claim{{Name: "mesh-dns-resolver", Scope: catalogue.ScopeMesh}}}
|
||||||
|
inv, ctx := aMeshWith(t, resolver)
|
||||||
|
if _, err := inv.SeedSeats(ctx, catalogue.DefaultSeats()); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
for _, n := range []string{"anchor", "home"} {
|
||||||
|
if _, err := inv.AddNode(ctx, n); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if _, err := inv.Assign(ctx, n, "resolver"); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := inv.HoldSeat(ctx, "mesh-dns-resolver", catalogue.ScopeMesh, "anchor", "resolver"); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
for range 2 {
|
||||||
|
if err := inv.AddSeatHolder(ctx, "mesh-dns-resolver", catalogue.ScopeMesh, "home", "resolver"); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
held, err := inv.Holdings(ctx)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if len(held) != 2 || held[0].Node != "anchor" || held[1].Node != "home" {
|
||||||
|
t.Fatalf("the two holders are not both on record, once each: %+v", held)
|
||||||
|
}
|
||||||
|
if err := inv.Unassign(ctx, "home", "resolver"); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if held, _ = inv.Holdings(ctx); len(held) != 1 || held[0].Node != "anchor" {
|
||||||
|
t.Fatalf("unassigning one holder took more or less than its own row: %+v", held)
|
||||||
|
}
|
||||||
|
if _, err := inv.Assign(ctx, "home", "resolver"); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := inv.AddSeatHolder(ctx, "mesh-dns-resolver", catalogue.ScopeMesh, "home", "resolver"); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := inv.HoldSeat(ctx, "mesh-dns-resolver", catalogue.ScopeMesh, "home", "resolver"); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if held, _ = inv.Holdings(ctx); len(held) != 1 || held[0].Node != "home" {
|
||||||
|
t.Fatalf("a handover left other holders on record: %+v", held)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+14
@@ -0,0 +1,14 @@
|
|||||||
|
-- A replicated seat has several holders on record (novox/hq ADR 0223).
|
||||||
|
--
|
||||||
|
-- 0039 recorded one holder per seat, keyed by the seat: a handover replaced the row, so the seat was
|
||||||
|
-- never without a holder in between. The mesh's resolver is now held on more than one machine, each
|
||||||
|
-- answering the same names, and each of those holders is recorded — added by `seat <name> --add`,
|
||||||
|
-- never by being assigned. So a holding is keyed by the seat and the assignment holding it.
|
||||||
|
--
|
||||||
|
-- Nothing else changes. A handover (`seat <name> --to`) still leaves exactly one row, replacing every
|
||||||
|
-- holder in one transaction; whether a seat may have more than one is the seat's compiled definition,
|
||||||
|
-- judged by the controller before a row is added, and a store holding two for any other seat is
|
||||||
|
-- refused at resolution, naming the seat. Every existing row is one per seat, so it satisfies the new
|
||||||
|
-- key as it stands.
|
||||||
|
alter table seat_holding drop constraint seat_holding_pkey;
|
||||||
|
alter table seat_holding add primary key (seat, node, module);
|
||||||
+37
-10
@@ -215,23 +215,50 @@ func (i *Inventory) RenameSeat(ctx context.Context, from, to string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// HoldSeat records that one assignment holds a seat, replacing whoever held it — as one write, so
|
// HoldSeat records that one assignment holds a seat, replacing whoever held it — every holder, for a
|
||||||
// the seat is never without a holder in between (novox/hq ADR 0131, design 28 task 5.3). The
|
// replicated seat (novox/hq ADR 0223) — as one transaction, so the seat is never without a holder in
|
||||||
// assignment must exist; the store refuses otherwise, and that refusal is the right one: a seat
|
// between (novox/hq ADR 0131, design 28 task 5.3). The assignment must exist; the store refuses
|
||||||
// cannot be handed to something that is not running anywhere.
|
// otherwise, and that refusal is the right one: a seat cannot be handed to something that is not
|
||||||
|
// running anywhere.
|
||||||
func (i *Inventory) HoldSeat(ctx context.Context, seat, scope, nodeName, module string) error {
|
func (i *Inventory) HoldSeat(ctx context.Context, seat, scope, nodeName, module string) error {
|
||||||
node, err := i.NodeByName(ctx, nodeName)
|
node, err := i.NodeByName(ctx, nodeName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
_, err = i.store.Pool().Exec(ctx,
|
tx, err := i.store.Pool().Begin(ctx)
|
||||||
`insert into seat_holding (seat, scope, node, module) values ($1, $2, $3, $4)
|
|
||||||
on conflict (seat) do update set scope = excluded.scope, node = excluded.node,
|
|
||||||
module = excluded.module, since = now()`,
|
|
||||||
seat, scope, node.ID, module)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer func() { _ = tx.Rollback(context.WithoutCancel(ctx)) }()
|
||||||
|
if _, err := tx.Exec(ctx,
|
||||||
|
`delete from seat_holding where seat = $1 and not (node = $2 and module = $3)`,
|
||||||
|
seat, node.ID, module); err != nil {
|
||||||
|
return fmt.Errorf("handing %s to %s on %s: %w", seat, module, nodeName, err)
|
||||||
|
}
|
||||||
|
if _, err := tx.Exec(ctx,
|
||||||
|
`insert into seat_holding (seat, scope, node, module) values ($1, $2, $3, $4)
|
||||||
|
on conflict (seat, node, module) do update set scope = excluded.scope, since = now()`,
|
||||||
|
seat, scope, node.ID, module); err != nil {
|
||||||
return fmt.Errorf("recording %s on %s as the holder of %s: %w", module, nodeName, seat, err)
|
return fmt.Errorf("recording %s on %s as the holder of %s: %w", module, nodeName, seat, err)
|
||||||
}
|
}
|
||||||
|
return tx.Commit(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddSeatHolder records one more assignment holding a replicated seat, beside those already on
|
||||||
|
// record (novox/hq ADR 0223). Whether the seat may have several holders is the caller's to judge —
|
||||||
|
// the seat's definition is compiled, and the store holds no column for it. Recording a holder
|
||||||
|
// already on record changes nothing.
|
||||||
|
func (i *Inventory) AddSeatHolder(ctx context.Context, seat, scope, nodeName, module string) error {
|
||||||
|
node, err := i.NodeByName(ctx, nodeName)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if _, err := i.store.Pool().Exec(ctx,
|
||||||
|
`insert into seat_holding (seat, scope, node, module) values ($1, $2, $3, $4)
|
||||||
|
on conflict (seat, node, module) do nothing`,
|
||||||
|
seat, scope, node.ID, module); err != nil {
|
||||||
|
return fmt.Errorf("adding %s on %s as a holder of %s: %w", module, nodeName, seat, err)
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -240,7 +267,7 @@ func (i *Inventory) HoldSeat(ctx context.Context, seat, scope, nodeName, module
|
|||||||
func (i *Inventory) Holdings(ctx context.Context) ([]catalogue.Held, error) {
|
func (i *Inventory) Holdings(ctx context.Context) ([]catalogue.Held, error) {
|
||||||
rows, err := i.store.Pool().Query(ctx,
|
rows, err := i.store.Pool().Query(ctx,
|
||||||
`select h.seat, h.scope, n.name, h.module, coalesce(n.site, '')
|
`select h.seat, h.scope, n.name, h.module, coalesce(n.site, '')
|
||||||
from seat_holding h join node n on n.id = h.node order by h.seat`)
|
from seat_holding h join node n on n.id = h.node order by h.seat, n.name, h.module`)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user