wgtunnel: S3 Part 2 — manager state machine + loop + desired raw-consumer seam
Manager: one-shot registration (marker gate; backoff cap 15m), adopt-lost-marker, re-key-on-mismatch, REVOKED-STAYS-REVOKED teardown (marker kept, zero execs on later ticks), no-teardown-on-absent-data, hash-gated apply (zero execs steady state), restart-not-reload on conf change, self-heal enable. Status stanza with latest-handshakes-ONLY wg read. Collector WireguardReporter seam. desired.Syncer AddConsumer fan-out with panic containment. Red-proofs a/b/d run + reverted; no-key-material-in-logs asserted. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PSK5g6qYLknKj8u3QAFEr6
This commit is contained in:
@@ -22,11 +22,28 @@ type Fetcher interface {
|
||||
FetchDesiredState(ctx context.Context) (*hub.DesiredStateResponse, error)
|
||||
}
|
||||
|
||||
// RawConsumer receives the FULL fetched desired-state document after each successful
|
||||
// generation-advance fetch (S3 seam — internal/wgtunnel consumes its wireguard block this way
|
||||
// without the reconcile engine learning about tunnels). Implementations must not block: do the
|
||||
// cheap store-and-nudge, never network/exec inline.
|
||||
type RawConsumer interface {
|
||||
OnDesiredState(ctx context.Context, resp *hub.DesiredStateResponse)
|
||||
}
|
||||
|
||||
// Syncer keeps the engine's CachingProvider in step with the hub's authoritative desired-state.
|
||||
type Syncer struct {
|
||||
fetcher Fetcher
|
||||
provider *reconcile.CachingProvider
|
||||
logger *slog.Logger
|
||||
fetcher Fetcher
|
||||
provider *reconcile.CachingProvider
|
||||
consumers []RawConsumer
|
||||
logger *slog.Logger
|
||||
}
|
||||
|
||||
// AddConsumer registers a raw desired-state consumer (nil-safe no-op). Not concurrency-safe —
|
||||
// call during wiring, before the hub loop starts.
|
||||
func (s *Syncer) AddConsumer(c RawConsumer) {
|
||||
if c != nil {
|
||||
s.consumers = append(s.consumers, c)
|
||||
}
|
||||
}
|
||||
|
||||
// NewSyncer builds a Syncer over the hub fetcher and the engine's provider.
|
||||
@@ -61,12 +78,27 @@ func (s *Syncer) OnEnvelope(ctx context.Context, env *hub.ControlEnvelope) {
|
||||
s.provider.Update(resp.Generation, state)
|
||||
s.logger.Info("desired: updated from hub",
|
||||
"generation", resp.Generation, "guests", len(state.Guests))
|
||||
// S3: fan the raw document out to registered consumers (wgtunnel etc). A panicking consumer
|
||||
// is contained — the guest reconcile path must never break over a tunnel add-on.
|
||||
for _, c := range s.consumers {
|
||||
s.notifyConsumer(ctx, c, resp)
|
||||
}
|
||||
if env.HasSignedOps {
|
||||
// 10A only notes the flag; fetching + verifying + executing signed ops is slice 10B.
|
||||
s.logger.Info("desired: hub reports pending signed ops (fetch/execute is slice 10B)")
|
||||
}
|
||||
}
|
||||
|
||||
// notifyConsumer delivers one raw document with panic containment.
|
||||
func (s *Syncer) notifyConsumer(ctx context.Context, c RawConsumer, resp *hub.DesiredStateResponse) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
s.logger.Error("desired: raw consumer panicked (contained)", "panic", r)
|
||||
}
|
||||
}()
|
||||
c.OnDesiredState(ctx, resp)
|
||||
}
|
||||
|
||||
// mapWire maps the hub wire desired-state to the reconcile domain. 10A acts only on guests; the
|
||||
// forward-compat fields (restore_directive — 10D — etc.) are carried on the wire and logged, but
|
||||
// not translated into actions here.
|
||||
|
||||
Reference in New Issue
Block a user