zester

Watchdog Runtime

The watchdog (cmd/zester-watchdog) is the node-local process responsible for child lifecycle supervision and atomic binary swaps.

Invocation

--component peel|master is the only flag you need — everything else is derived from the component and the child's config. The packaged systemd units are simply:

ExecStart=/usr/local/bin/zester-watchdog --component peel
# or
ExecStart=/usr/local/bin/zester-watchdog --component master

Any derived value can be overridden by passing the flag explicitly (explicit always wins).

What --component derives

Source: deriveComponentDefaults in cmd/zester-watchdog/derive.go, run after flag.Parse (explicit flags are detected via flag.Visit).

Derived flagpeelmaster
--child-bin/usr/local/bin/zester-peel/usr/local/bin/zester-master
--child-args--config /etc/zester/peel.yaml--config /etc/zester/master.yaml
--idid from peel.yaml, else the hostname (sanitized)the hostname (sanitized)
--nats-creds<auth_dir>/<id>.creds<auth_dir>/master.creds
--nats-caconfig nats_ca, else <auth_dir>/nats-ca.crtsame
--bootstrap-cache<data_dir>/nats-bootstrap.msgpack(none)
--health-urlhttp://<health_addr>/healthz (:9090)http://<health_addr>/healthz (:9091)

auth_dir, data_dir, health_addr, and nats_ca are read from the child's config (a missing config falls back to the per-component defaults, never fatal). The watchdog resolves --id from the same source the peel does (config.ResolveNodeID), so the two can never disagree about the node's identity — no separate ZESTER_PEEL_ID to keep in sync.

Node id = hostname, sanitized and pinned

With no id in peel.yaml, the id defaults to the machine hostname (hostname -f). On the wire it becomes a NATS subject token with . encoded as _ (web01.example.comweb01_example_com — lossless, since a valid hostname never contains _, and web01.pl/web01-pl stay distinct); the CLI displays and accepts the dotted human form everywhere. The derived id is pinned to <auth_dir>/node-id on first resolution (Salt minion_id semantics): hostname -f depends on DNS and would otherwise silently fall back to the short kernel name during a resolver outage, re-identifying the node on reboot. The pin also guarantees the watchdog and the peel converge on one identity. An explicit id is never pinned; to re-derive from the hostname, delete the file. A derived id that is a localhost placeholder (localhost, localhost.localdomain, …) is refused at startup — set an explicit id.

Colocated master + peel

Both watchdogs on one host share the node id and therefore the command subject zester.update.cmd.<id>. Every update command carries the target component, and a watchdog silently drops commands for the other component — a peel rollout can never swap the master binary.

Non-derived / override flags

FlagDefaultNotes
--nats-urltls://localhost:4222Not derived (the peel's discovery sentinel is empty; a master's config default differs)
--ready-urlderived from --health-url/readyzPolled only during update soak; restart monitoring stays on --health-url liveness
--health-timeout5sHealth check timeout
--health-interval10sHealth check interval
--health-retries3Consecutive health failures before rollback
--soak-time60sPost-update soak period
--log-level / --log-formatinfo / json

Startup Sequence

Source of truth: cmd/zester-watchdog/main.go.

  1. Build SlotManager on --child-bin and run Recover().
  2. Start supervised child process (best effort).
  3. Start AutoRestart() loop.
  4. Connect to NATS (wait for creds file when configured).
  5. Start update command handler on zester.update.cmd.<id>.
  6. Start status reporter to update-status KV.

Slot Layout and Atomic Swap

Source of truth: pkg/update/slots.go.

  • Current binary: <basePath>
  • Previous binary: <basePath>.prev
  • Staging binary: <basePath>.staging

Apply behavior:

  1. remove existing .prev (if present)
  2. rename current -> .prev
  3. rename .staging -> current
  4. if step 3 fails, attempt rename .prev back to current

Recovery behavior on startup:

  • If .staging exists and current is missing: move .staging -> current.
  • If .prev exists and current is missing: move .prev -> current.

Update Command Protocol

Source of truth: pkg/update/handler.go.

Request (UpdateCommand)

FieldTypeNotes
commandstringprepare, apply, confirm, rollback, status
versionstringTarget version
componentstringpeel or master
sha256stringExpected binary digest
object_keystringObject Store key

Response (UpdateResponse)

FieldTypeNotes
statusstringResult status (error, staged, applying, confirmed, etc.)
versionstringPending/confirmed version
hashstringStaged digest
errorstringError details
statestringHandler state (for status)
uptimestringChild uptime string (for status)

Handler State Machine

States from pkg/update/handler.go:

  • idle
  • preparing
  • staged
  • applying
  • soaking
  • confirmed
  • rolling_back

Valid command transitions:

CommandAllowed state(s)Result
prepareidle, confirmedDownload + stage binary, move to staged
applystagedSwap slots, restart child, enter soaking
confirmsoakingCancel soak goroutine, cleanup staging, move to confirmed
rollbackstaged, applying, soakingRestore previous binary, restart child, move to idle
statusanyReturn current handler state + uptime

Soak and Health Policy

  • apply starts a background soak routine.
  • Soak behavior:
    • Wait for WaitForHealthy() success first (liveness, --health-url).
    • Then poll readiness (Supervisor.CheckReady against --ready-url; falls back to the health URL when unset) for the configured soak duration. Readiness catches a child that is alive but functionally dead — e.g. NATS-disconnected, where /readyz returns 503 while /healthz stays 200.
    • Consecutive readiness failures up to HealthRetries trigger auto-rollback; a successful probe resets the failure counter.
  • Restart monitoring and WaitForHealthy stay on liveness — a NATS outage never restarts a healthy child; only the soak decision uses readiness.
  • confirm or rollback cancels soak monitoring via context cancellation.

Confirm Deadline

Entering soaking also arms a confirm-deadline covering the whole window (WaitForHealthy + soak + awaiting confirm): HandlerConfig.ConfirmDeadline, defaulting to 3× SoakTime floored at MinConfirmDeadline (5 minutes). If neither confirm nor rollback arrives from the controller before it expires — e.g. the driving master died mid-batch — the handler logs an Error (no confirm or rollback from controller before deadline, auto-rolling back) and rolls back to the previous binary, returning to idle. Rolling back is the safe default: an unconfirmed node also rejects all future prepares, so without the deadline it would run an unconfirmed binary forever. Readiness checks still gate only the soak window itself — after the soak passes, the handler simply waits for confirm or the deadline.

Caveat: if NATS goes down while a node is inside its soak window, that node's /readyz reports down and the update rolls back even though the binary is fine. Avoid starting rollouts during planned NATS maintenance.

Supervisor Policy

Source of truth: pkg/update/supervisor.go.

  • Child process start uses separate process group (Setpgid: true).
  • Stop behavior: SIGTERM then up to 10s wait, then SIGKILL.
  • Health/readiness probe expectations (checkEndpoint, shared by CheckHealth and CheckReady):
    • HTTP 200
    • JSON body with status: "ok" or "degraded" (case-insensitive) — degraded means working-but-impaired and is not probe-fatal
  • AutoRestart():
    • exponential backoff restart (2^n seconds, capped at 60s)
    • after 10 consecutive failures, enters a degraded slow-retry tier (not terminal): restart attempts continue every SupervisorConfig.DegradedRetryInterval (default 10m), and degraded state clears automatically once a restarted child survives the stability window
    • failure counter (and degraded state) resets after 30s stable runtime

Status Reporting

Source of truth: pkg/update/status.go.

  • Reporter writes NodeStatus entries to update-status every 30s (default).
  • Key format: <component>.<id>.
  • Includes runtime fields such as version, state, GOOS/GOARCH, child PID, uptime, and updated_at.
  • NodeStatus.Degraded (msgpack degraded, additive) reports whether the supervisor is in the degraded slow-retry tier, so a persistently failing child is visible in fleet status. Degraded nodes are excluded from rollouts by the master's rollout controller.
  • NodeStatus.Protocol (msgpack protocol,omitempty, additive) carries the node's update protocol number — the watchdog stamps proto.ProtocolVersion (currently 1) into every report. The rollout controller checks it against Manifest.MinProtocol before starting a rollout; a decoded 0 means the field was never set and is treated as compatible (it fails only an explicit MinProtocol > 0 gate).
  • zester update status surfaces both as the trailing DEGRADED (yes/no) and PROTO (- = protocol 0, never reported) columns.

On this page