Report what an adopted node holds, its firewall and what is reachable, and speak unasked when that changes (hq ADR 0100)
This commit is contained in:
+99
-3
@@ -20,6 +20,7 @@ import (
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
"text/tabwriter"
|
||||
"time"
|
||||
@@ -27,10 +28,12 @@ import (
|
||||
"github.com/novox/mesh-host/internal/apply"
|
||||
"github.com/novox/mesh-host/internal/bundle"
|
||||
"github.com/novox/mesh-host/internal/declaration"
|
||||
"github.com/novox/mesh-host/internal/firewall"
|
||||
"github.com/novox/mesh-host/internal/identity"
|
||||
"github.com/novox/mesh-host/internal/inventory"
|
||||
"github.com/novox/mesh-host/internal/link"
|
||||
"github.com/novox/mesh-host/internal/profile"
|
||||
"github.com/novox/mesh-host/internal/reachable"
|
||||
"github.com/novox/mesh-host/internal/store"
|
||||
"github.com/novox/mesh-host/internal/system"
|
||||
"github.com/novox/mesh-host/internal/upgrade"
|
||||
@@ -437,6 +440,23 @@ func enrol(ctx context.Context, opts options) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// An adopted node keeps the firewall it was found with (novox/hq ADR 0100), so a host that
|
||||
// cannot speak that firewall must say so now — before the mesh records a node it could never
|
||||
// open anything on.
|
||||
if token.Adopted {
|
||||
kind, name, err := firewall.Detect(ctx, apply.ExecRunner)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if kind == firewall.Unsupported {
|
||||
return fmt.Errorf(
|
||||
"this token joins this machine adopted, keeping the firewall found on it, and it is "+
|
||||
"filtered by %s, which no host speaks yet. Nothing was enrolled", name)
|
||||
}
|
||||
fmt.Printf("joining adopted: what is on this machine is kept, and its firewall (%s) stays in force\n",
|
||||
string(kind))
|
||||
}
|
||||
|
||||
fmt.Printf("token for broker %s\n", token.Broker)
|
||||
fmt.Printf(" pinned certificate %s\n", token.Fingerprint)
|
||||
fmt.Printf(" signing key %s\n",
|
||||
@@ -616,7 +636,22 @@ func runLink(ctx context.Context, opts options) error {
|
||||
// new declarations; this holds the machine in the last one whether the link is up or not. A
|
||||
// laptop shut for a week comes back and reconciles — it does not come back and ask what it is
|
||||
// (novox/hq ADR 0004).
|
||||
go holdTheMachine(ctx, opts, mine, say, sched)
|
||||
// Reports a reconcile has to make unasked — what an adopted node holds changed, or its
|
||||
// firewall did — go out over the link when it is up (novox/hq ADR 0100).
|
||||
outbox := make(chan link.Report, 1)
|
||||
watch := &adoptionWatch{}
|
||||
applier = watch.noting(applier)
|
||||
go holdTheMachine(ctx, opts, mine, say, sched, func(r link.Report) {
|
||||
if !watch.changed(r) {
|
||||
return
|
||||
}
|
||||
select {
|
||||
case <-outbox:
|
||||
// An older one nobody has published yet; this one says everything it did.
|
||||
default:
|
||||
}
|
||||
outbox <- r
|
||||
})
|
||||
|
||||
return link.HoldRoused(ctx, link.Membership{
|
||||
Node: mine.Node,
|
||||
@@ -624,7 +659,46 @@ func runLink(ctx context.Context, opts options) error {
|
||||
Fingerprint: mine.Membership.Fingerprint,
|
||||
Password: mine.Membership.Password,
|
||||
Signer: mine.Membership.Signer,
|
||||
}, applier, say, opts.timeout, rousedBySignal(ctx))
|
||||
}, applier, say, opts.timeout, rousedBySignal(ctx), outbox)
|
||||
}
|
||||
|
||||
// adoptionWatch remembers what the node last said about what it holds and its firewall, so a
|
||||
// reconcile speaks unasked only when that changed.
|
||||
type adoptionWatch struct {
|
||||
mu sync.Mutex
|
||||
last string
|
||||
}
|
||||
|
||||
// fingerprint is what a report says about adoption: each hold and whether it changed, and the
|
||||
// firewall.
|
||||
func adoptionFingerprint(r link.Report) string {
|
||||
parts := []string{"firewall=" + r.Firewall}
|
||||
for _, h := range r.Held {
|
||||
parts = append(parts, h.ID+"="+h.Changed)
|
||||
}
|
||||
sort.Strings(parts[1:])
|
||||
return strings.Join(parts, "\n")
|
||||
}
|
||||
|
||||
// changed records a report and says whether it differs from the last one that went out.
|
||||
func (w *adoptionWatch) changed(r link.Report) bool {
|
||||
w.mu.Lock()
|
||||
defer w.mu.Unlock()
|
||||
now := adoptionFingerprint(r)
|
||||
if now == w.last {
|
||||
return false
|
||||
}
|
||||
w.last = now
|
||||
return true
|
||||
}
|
||||
|
||||
// noting wraps the applier, so a report the link publishes after a delivery counts as said.
|
||||
func (w *adoptionWatch) noting(apply link.Applier) link.Applier {
|
||||
return func(ctx context.Context, raw, signature []byte) link.Report {
|
||||
r := apply(ctx, raw, signature)
|
||||
w.changed(r)
|
||||
return r
|
||||
}
|
||||
}
|
||||
|
||||
// rousedBySignal is the machine telling this process that its link is probably stale.
|
||||
@@ -672,7 +746,7 @@ func rousedBySignal(ctx context.Context) link.Roused {
|
||||
const ReconcileEvery = 5 * time.Minute
|
||||
|
||||
func holdTheMachine(ctx context.Context, opts options, mine identity.Identity, say link.Announce,
|
||||
sched *apply.Scheduler) {
|
||||
sched *apply.Scheduler, publish func(link.Report)) {
|
||||
ticker := time.NewTicker(ReconcileEvery)
|
||||
defer ticker.Stop()
|
||||
|
||||
@@ -695,6 +769,12 @@ func holdTheMachine(ctx context.Context, opts options, mine identity.Identity, s
|
||||
}
|
||||
|
||||
report := applyDeclared(ctx, opts, declared, sched)
|
||||
// A reconcile is otherwise silent. On an adopted node it speaks when what it holds or
|
||||
// its firewall changed, because that is how a predecessor still writing is caught
|
||||
// (novox/hq ADR 0100); publish decides whether anything did.
|
||||
if publish != nil && report.Refused == "" && (len(report.Held) > 0 || report.Firewall != "") {
|
||||
publish(report)
|
||||
}
|
||||
switch {
|
||||
case report.Refused != "":
|
||||
say("what this node was last told no longer applies: " + report.Refused)
|
||||
@@ -761,6 +841,22 @@ func applyAndKeep(ctx context.Context, opts options, raw []byte, signed *store.D
|
||||
}
|
||||
|
||||
report := link.Report{Carried: carriedPorts(updated), Declared: digestOf(raw)}
|
||||
// What this node found and holds, its firewall, and what is reachable on it — so an adopted
|
||||
// node never reads as converged (novox/hq ADR 0100).
|
||||
for _, h := range updated.Held {
|
||||
report.Held = append(report.Held, link.Held{ID: h.ID, Module: h.Module, Kind: h.Kind,
|
||||
Target: h.Target, Since: h.Since, Changed: h.Changed, Kept: h.Kept})
|
||||
}
|
||||
if declared.Adoption != nil {
|
||||
if updated.Firewall != nil {
|
||||
report.Firewall = updated.Firewall.Kind
|
||||
}
|
||||
reached, err := reachable.Collect(ctx, apply.ExecRunner)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "mesh-host: applied, and could not read what is reachable here: %v\n", err)
|
||||
}
|
||||
report.Reachable = reached
|
||||
}
|
||||
for _, change := range outcome.Outcomes {
|
||||
// What is held is not what this machine owns: it was found, and is kept as it was until
|
||||
// its module is taken (novox/hq ADR 0100).
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/novox/mesh-host/internal/link"
|
||||
"github.com/novox/mesh-host/internal/store"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -135,3 +137,35 @@ func TestAFlagAfterAPositionalIsRead(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Defends novox/hq ADR 0100: a reconcile on an adopted node speaks unasked only when what it holds
|
||||
// or its firewall changed — which is how a predecessor still writing is caught, without a report
|
||||
// every five minutes saying nothing new.
|
||||
func TestAReconcileSpeaksOnlyWhenWhatIsHeldChanged(t *testing.T) {
|
||||
w := &adoptionWatch{}
|
||||
held := link.Report{Firewall: "ufw", Held: []link.Held{{ID: "hello-web.page"}, {ID: "hello-web.server"}}}
|
||||
if !w.changed(held) {
|
||||
t.Fatal("the first report of a hold was not said")
|
||||
}
|
||||
again := link.Report{Firewall: "ufw", Held: []link.Held{{ID: "hello-web.server"}, {ID: "hello-web.page"}}}
|
||||
if w.changed(again) {
|
||||
t.Error("the same holds in another order were said again")
|
||||
}
|
||||
rewritten := link.Report{Firewall: "ufw", Held: []link.Held{{ID: "hello-web.page", Changed: "rewritten"}, {ID: "hello-web.server"}}}
|
||||
if !w.changed(rewritten) {
|
||||
t.Error("a held file rewritten by something else was not said")
|
||||
}
|
||||
if !w.changed(link.Report{Firewall: "none", Held: rewritten.Held}) {
|
||||
t.Error("a changed firewall was not said")
|
||||
}
|
||||
}
|
||||
|
||||
func TestWhatTheLinkPublishedCountsAsSaid(t *testing.T) {
|
||||
w := &adoptionWatch{}
|
||||
report := link.Report{Firewall: "ufw", Held: []link.Held{{ID: "a"}}}
|
||||
applier := w.noting(func(context.Context, []byte, []byte) link.Report { return report })
|
||||
applier(context.Background(), nil, nil)
|
||||
if w.changed(report) {
|
||||
t.Error("a reconcile repeated what the link had just published")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user