diff --git a/cmd/mesh-host/main.go b/cmd/mesh-host/main.go index 8eeb3ce..6435317 100644 --- a/cmd/mesh-host/main.go +++ b/cmd/mesh-host/main.go @@ -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) } @@ -973,7 +974,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 +1014,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 +1383,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) +} diff --git a/internal/identity/identity.go b/internal/identity/identity.go index c8d7430..e01e522 100644 --- a/internal/identity/identity.go +++ b/internal/identity/identity.go @@ -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.