Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
68a193d6f0 | ||
|
|
4fba884d47 | ||
|
|
15f0dabf32 | ||
|
|
4ef41ad5a0 | ||
|
|
d749de989c | ||
|
|
14e64844df | ||
|
|
d82fc9a121 | ||
|
|
9fd3762e93 | ||
|
|
587b8aa220 |
+67
-3
@@ -869,6 +869,7 @@ func overlayCommand(ctx context.Context, opts options) error {
|
||||
if err := link.Publish(ctx, link.Membership{
|
||||
Node: mine.Node, Broker: mine.Membership.Broker, Fingerprint: mine.Membership.Fingerprint,
|
||||
Password: mine.Membership.Password, Signer: mine.Membership.Signer,
|
||||
Transport: mine.Membership.Transport,
|
||||
}, link.Report{Node: mine.Node, Rekey: &rekey}, opts.timeout); err != nil {
|
||||
return fmt.Errorf("the mesh could not be told; nothing was written here: %w", err)
|
||||
}
|
||||
@@ -961,9 +962,14 @@ func runLink(ctx context.Context, opts options) error {
|
||||
return nil // asked to stop while waiting
|
||||
}
|
||||
|
||||
fmt.Printf("node %s, linking to %s\n", mine.Node, mine.Membership.Broker)
|
||||
|
||||
// **A membership delivered while this host was not running is adopted before the first dial.**
|
||||
// The ordinary path is a declaration, read after it applies; the rescue path is an operator
|
||||
// writing the file by hand on a machine no bus can reach — rotated while it held the old
|
||||
// password, say — and restarting the host (design 28, task 5.2). Same file, same check.
|
||||
say := func(line string) { fmt.Println(line) }
|
||||
adoptDeliveredMembership(identity.Path(opts.state), &mine, say)
|
||||
|
||||
fmt.Printf("node %s, linking to %s\n", mine.Node, mine.Membership.Broker)
|
||||
|
||||
// One scheduler for the life of the process, re-established from each applied declaration
|
||||
// (novox/hq ADR 0053). It fires scheduled steps on their cadence, surviving across applies and
|
||||
@@ -973,7 +979,12 @@ func runLink(ctx context.Context, opts options) error {
|
||||
go sched.Run(ctx)
|
||||
|
||||
applier := func(ctx context.Context, raw, signature []byte) link.Report {
|
||||
return applyAndKeep(ctx, opts, raw, &store.Declared{Declaration: raw, Signature: signature}, sched, say)
|
||||
report := applyAndKeep(ctx, opts, raw, &store.Declared{Declaration: raw, Signature: signature}, sched, say)
|
||||
// **A declaration may carry this machine's membership for another bus.** It arrives as a
|
||||
// sealed file like any secret, and is read after the rest has applied so the bus it names is
|
||||
// standing before this machine leaves the one it is on (novox/hq design 28, task 5.2).
|
||||
adoptDeliveredMembership(identity.Path(opts.state), &mine, say)
|
||||
return report
|
||||
}
|
||||
|
||||
// Two things at once, and the second is what makes disconnection ordinary. The link brings
|
||||
@@ -1008,6 +1019,7 @@ func runLink(ctx context.Context, opts options) error {
|
||||
Broker: mine.Membership.Broker,
|
||||
Fingerprint: mine.Membership.Fingerprint,
|
||||
Password: mine.Membership.Password,
|
||||
Transport: mine.Membership.Transport,
|
||||
Signer: mine.Membership.Signer,
|
||||
}, applier, say, opts.timeout, rousedBySignal(ctx), outbox)
|
||||
}
|
||||
@@ -1376,3 +1388,55 @@ func carriedPorts(state store.State) []int {
|
||||
sort.Ints(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// MembershipNextPath is where the mesh delivers this machine's membership for the bus it is moving
|
||||
// to: a sealed file in a declaration, written by the host like any secret, read here after the
|
||||
// declaration has applied.
|
||||
const MembershipNextPath = "/var/lib/mesh/membership-next.json"
|
||||
|
||||
// adoptDeliveredMembership moves this machine to the bus a delivered membership names.
|
||||
//
|
||||
// **Saved, then restarted — not swapped in place.** The link holds one membership for the life of
|
||||
// the process, and every reconnect path assumes the bus did not change under it; a process that
|
||||
// found itself half on one bus and half on another would be a new kind of state nothing was written
|
||||
// for. Exiting cleanly hands the machine to the service manager's restart, and the process that
|
||||
// comes back reads the identity file the way it always has and dials the bus it names. That is the
|
||||
// same path a machine takes after a reboot, which is why nothing new has to be right for it to work.
|
||||
//
|
||||
// A membership identical to the one held is nothing: the file stays and is read again next time.
|
||||
func adoptDeliveredMembership(identityPath string, mine *identity.Identity, say func(string)) {
|
||||
raw, err := os.ReadFile(MembershipNextPath)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
var next identity.Membership
|
||||
if err := json.Unmarshal(raw, &next); err != nil {
|
||||
say(fmt.Sprintf("a membership was delivered at %s and could not be read: %v", MembershipNextPath, err))
|
||||
return
|
||||
}
|
||||
if next.Broker == "" || next.Password == "" || next.Fingerprint == "" {
|
||||
say(fmt.Sprintf("a membership was delivered at %s with no broker, fingerprint or password; ignored", MembershipNextPath))
|
||||
return
|
||||
}
|
||||
same := next.Broker == mine.Membership.Broker && next.Fingerprint == mine.Membership.Fingerprint &&
|
||||
next.Password == mine.Membership.Password && next.Transport == mine.Membership.Transport
|
||||
if same {
|
||||
return
|
||||
}
|
||||
// The signer is the mesh's, not the bus's: a delivered membership that names none keeps the one
|
||||
// this machine already trusts, because a change of bus is not a change of who signs declarations.
|
||||
if len(next.Signer) == 0 {
|
||||
next.Signer = mine.Membership.Signer
|
||||
}
|
||||
mine.Membership = next
|
||||
if err := identity.Save(identityPath, *mine); err != nil {
|
||||
say(fmt.Sprintf("a membership for another bus was delivered and could not be saved: %v", err))
|
||||
return
|
||||
}
|
||||
transport := next.Transport
|
||||
if transport == "" {
|
||||
transport = "the current"
|
||||
}
|
||||
say(fmt.Sprintf("moving to %s bus at %s — restarting to dial it", transport, next.Broker))
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
@@ -152,7 +152,7 @@
|
||||
"type": "file",
|
||||
"path": "/var/lib/mesh-bus-conf/accounts.conf",
|
||||
"mode": "0600",
|
||||
"content": "// The first user list, carried by the installer because at genesis there is no mesh to\n// compose one. A bootstrap credential, rotated with the store's and replaced by the\n// controller's own composition from its first start onward.\naccounts {\n MESH {\n users = [\n { user: \"controller\", password: \"$2a$10$AHqJgOifIVbU41KmATiMhuXFs8xa7Wl2HuN4UVBCXdN2jIQzjqApy\", permissions: {\n publish: { allow: [\"$JS.API.>\", \"$JS.ACK.CONTROL.controller.>\", \"$JS.ACK.EVENTS.controller.>\", \"_INBOX.enrol.>\", \"mesh.control.>\", \"mesh.node.>\", \"mesh.seat.mesh-build-machine.accept.>\"] }\n subscribe: { allow: [\"$JS.API.>\", \"_INBOX.controller.>\", \"mesh.control.>\", \"mesh.mod.mesh-catalog.event.catching-up\", \"mesh.mod.mesh-catalog.event.upgraded\", \"mesh.seat.mesh-build-machine.event.built\"] }\n allow_responses: { max: 1, ttl: \"1m\" }\n } }\n ]\n }\n}\n"
|
||||
"content": "// The first user list, carried by the installer because at genesis there is no mesh to\n// compose one. A bootstrap credential, rotated with the store's and replaced by the\n// controller's own composition from its first start onward.\naccounts {\n MESH {\n jetstream: enabled\n users = [\n { user: \"controller\", password: \"$2a$10$AHqJgOifIVbU41KmATiMhuXFs8xa7Wl2HuN4UVBCXdN2jIQzjqApy\", permissions: {\n publish: { allow: [\"$JS.API.>\", \"$JS.ACK.CONTROL.controller.>\", \"$JS.ACK.EVENTS.controller.>\", \"_INBOX.enrol.>\", \"mesh.control.>\", \"mesh.node.>\", \"mesh.seat.mesh-build-machine.accept.>\"] }\n subscribe: { allow: [\"$JS.API.>\", \"_DELIVER.controller\", \"_DELIVER.controller.>\", \"_INBOX.controller.>\", \"mesh.control.>\", \"mesh.mod.mesh-catalog.event.catching-up\", \"mesh.mod.mesh-catalog.event.upgraded\", \"mesh.seat.mesh-build-machine.event.built\"] }\n allow_responses: { max: 1, ttl: \"1m\" }\n } }\n ]\n }\n}\n"
|
||||
},
|
||||
{
|
||||
"id": "broker",
|
||||
|
||||
@@ -76,6 +76,11 @@ type Membership struct {
|
||||
// Not the token's secret: that is spent, and a credential that lives for ever should not be
|
||||
// the same string as one that was meant to be used once.
|
||||
Password string `json:"password"`
|
||||
|
||||
// Transport is which bus this membership is for. Empty is the bus the mesh ran on before
|
||||
// the move — so every membership written before this field existed reads as correct, not as
|
||||
// unset — and "nats" is the one being moved to (novox/hq design 28, task 5.2).
|
||||
Transport string `json:"transport,omitempty"`
|
||||
}
|
||||
|
||||
// Queue is where this node listens. Its account may read this and nothing else.
|
||||
|
||||
@@ -70,7 +70,13 @@ func dialNats(ctx context.Context, m Membership, timeout time.Duration) (Link, e
|
||||
}
|
||||
opts := []nats.Option{
|
||||
nats.Secure(config),
|
||||
nats.UserInfo(m.Node, m.Password),
|
||||
// The mesh names a machine's bus user "node.<name>" (the controller's principal scheme), and
|
||||
// the server refused the bare name the first time a machine dialled it: "authentication
|
||||
// error - User". The same string the mesh composed into the user list, or nothing connects.
|
||||
nats.UserInfo("node."+m.Node, m.Password),
|
||||
// Replies to what this client asks the server arrive on its inbox, and the mesh grants a
|
||||
// machine exactly its own: the same prefix the user list was composed with.
|
||||
nats.CustomInboxPrefix("_INBOX.node." + m.Node),
|
||||
nats.Name("mesh-host/" + m.Node),
|
||||
nats.Timeout(timeout),
|
||||
// A node that has silently lost its route notices, rather than holding a connection the
|
||||
|
||||
Reference in New Issue
Block a user