wheelhouse-agent#
One binary does three jobs: it is the daemon that serves the web UI and the API, and it
is also plan and apply, which diff a
desired-state file against the router from a shell, a cron
job or a CI step. With no subcommand it is the daemon. Every subcommand shares the same
flag set, so --api-url and --api-key-file mean the same thing in all three.
$ wheelhouse-agent version
wheelhouse-agent 0.5.1Synopsis#
wheelhouse-agent [flags] [command]
commands:
plan print the set/delete commands that would bring the router to the desired file (exit 2 if any)
apply stage them in a running agent (--agent-url), or commit with --commit --confirm-minutes N
run 'wheelhouse-agent --help' for the full flag listThat text is usageLine() in agent/desired.go, printed on an
unknown command or an unexpected argument.
The three commands#
| Command | What it does | Needs a licence |
|---|---|---|
| (none) | Runs the daemon: opens the state file, loads the catalogue, bootstraps the first admin account, starts the listener, the throughput sampler, the licence refresher, the cache primer and — if --reconcile-file is set — the reconcile loop. | No, but the write plane answers 402 without one |
plan | Reads the desired-state file, reads the live configuration, prints the set/delete commands that would close the gap. Changes nothing. | No |
apply | The same diff, then either stages it in a running agent (--agent-url) or commits it (--commit --confirm-minutes N). | Yes |
version | Prints wheelhouse-agent <version> and exits 0, before any config is read or any secret file is opened. | No |
version is handled as a subcommand and as the --version flag, and both are
answered first thing in main() — deliberately, because "what version are you on" is
the first question of every support call and a router that will not start still has to
be able to answer it (agent/main.go).
Flags and the subcommand may interleave#
The subcommand is not required to come first, and it is not required to come last.
splitSubcommand walks the command line and asks the flag table whether each bare word
is a flag's value or the command itself, so all four of these are the same run:
wheelhouse-agent plan --file /config/wheelhouse/agent.yaml
wheelhouse-agent --file /config/wheelhouse/agent.yaml plan
wheelhouse-agent --json plan --file /config/wheelhouse/agent.yaml
wheelhouse-agent --file=/config/wheelhouse/agent.yaml plan --jsonA boolean flag stands alone (--json, not --json true), which the parser discovers
through the flag's own IsBoolFlag method rather than by guessing. An argument that is
neither a flag nor a known command is refused:
$ wheelhouse-agent --json planx
wheelhouse: unknown command "planx"
usage: wheelhouse-agent [flags] [command]
...Exit status 2. See Exit codes.
Starting the daemon by hand#
Either --demo, or both --api-url and an API key, are required; the agent refuses to
start otherwise. On an appliance the shipped systemd unit supplies all of it — see
systemd units — so this form is for a lab:
wheelhouse-agent \
--api-url https://127.0.0.1 \
--api-key-file /etc/wheelhouse/api-key \
--addr 127.0.0.1:8090 \
--data-dir /config/wheelhouse \
--ui-dir /opt/wheelhouse/ui \
--log-level infoOn the very first start with an empty data directory the agent creates the admin
account and logs its generated password exactly once:
journalctl -u wheelhouse-agent | grep "shown once"The same password is written to <data-dir>/initial-password at mode 0600 so the
console banner can show it, and removed as soon as that
account's password is changed.
plan#
wheelhouse-agent \
--api-url https://127.0.0.1 --api-key-file /config/wheelhouse/api-key \
plan --file /config/wheelhouse/agent.yamlPrints one set or delete line per operation on stdout, a count on stderr, and any
advisory warnings on stderr before them. --json swaps the lines for a JSON document
with file, full, ops, count and warnings. --full also emits the deletes for
live nodes the file does not declare.
Exit 0 means the router already matches the file; exit 2 means it does not, the way
diff -q behaves, so it drops into a shell test or a CI gate without parsing text. Exit
1 is a real failure — the file will not parse, or the router will not answer.
apply#
apply needs a usable licence key, given as --license-key-file because a one-shot
command has no state file to read one from. Without one it prints
wheelhouse: apply needs a usable licence key (--license-key-file); plan works without oneand exits 1.
It then takes one of two routes:
# Route 1: hand the ops to a running agent's Commit Bar. A human still commits.
wheelhouse-agent apply \
--file /config/wheelhouse/agent.yaml \
--agent-url https://127.0.0.1:8443 \
--admin-token-file /config/wheelhouse/admin-token \
--license-key-file /config/wheelhouse/license-key
# Route 2: commit directly, behind the router's own commit-confirm.
wheelhouse-agent apply \
--file /config/wheelhouse/agent.yaml \
--commit --confirm-minutes 2 \
--license-key-file /config/wheelhouse/license-keyRoute 1 posts to the running agent's POST /api/stage over HTTPS with the break-glass
token as the credential, skipping certificate verification because the agent's listener
is routinely self-signed on loopback. Without --admin-token or --admin-token-file it
refuses. Without --agent-url and without --commit it also refuses, because staging
into this process's own memory would look like it worked and then evaporate on exit.
Route 2 refuses to run without --confirm-minutes N: a mistake should be revertible.
After the commit it re-reads the configuration; if the router still answers, the change
did not cut the agent off, so it confirms — the way an operator does when the page still
loads. If the router has stopped answering it deliberately does not confirm, and
says so, leaving the router to roll back on its own timer.
What the daemon starts#
| Goroutine | When | Source |
|---|---|---|
| Throughput sampler | Always. One show interfaces counter read every 5 s, 240 samples kept per interface in memory. | newHistory(240, 5*time.Second) |
| Licence refresher | Always. First attempt 20 s after start, then daily; hourly after a failure. | runLicenseRefresher |
| Cache primer | Unless --demo. Warms the read cache at start and after every commit. | runPrimer |
| Reconcile loop | Only when --reconcile-file is set. | reconciles.Run |
On SIGTERM or SIGINT the agent stops those, then gives in-flight requests 15 seconds
to drain rather than cutting an operator off mid-commit.
See also#
- Every flag — all 51, with defaults.
- Environment variables — the six the agent reads.
- Exit codes
- The desired-state file — the format
planandapplyread. - systemd units — how the appliance starts the daemon.
install.sh— the lab installer that wraps all this.- Deployment modes — on the router, or beside it.
- Developer install
Checked against#
agent/main.go (main, registerFlags, splitSubcommand,
runDaemon, bootstrapAdmin, loadSecretFiles),
agent/desired.go (runPlanApply, stageViaAgent,
usageLine),
agent/metrics.go,
agent/primer.go,
docs/deploy.md.