Building the agent#
wheelhouse-agent is one Go main package with no sub-packages, no code generation and
five direct dependencies. Building it is a single go build; the release adds
-trimpath, strips the symbol table, and stamps the version through the linker. This
page gives the exact commands CI runs, explains the two variables that are set at link
time, describes the three ways the binary can be invoked, and maps every source file to
what it is responsible for.
The build#
cd agent
go build -o wheelhouse-agent .That is enough for development. The release build, from
.forgejo/workflows/ci.yml, is:
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -trimpath \
-ldflags="-s -w -X main.version=1.2.3" -o wheelhouse-agent-amd64 .| Part | Why |
|---|---|
CGO_ENABLED=0 | A static binary that runs on any glibc, including the image's |
-trimpath | Keeps the build host's directory names out of the binary |
-s -w | Drops the symbol table and DWARF; the binary is smaller and no less debuggable in the ways that matter here |
-X main.version=… | Stamps the version. Without it the binary reports the constant in agent/main.go |
CI builds amd64 and arm64 from the same source in a loop. There is no arm64 image —
the arm64 package exists so the agent can run off-router against an arm64 router.
The two link-time variables#
main.version defaults to the string in agent/main.go. It is what
wheelhouse-agent version prints, what GET /api/version reports, and what the release
process is expected to keep in step with the tag.
main.licensePublicKeys is kid:base64[,kid:base64] — the licence server's public
keys, by key id, compiled in so a router with no route to the internet can still verify
what it holds. The default is the production server's public key, which is public and
therefore fine in source (agent/license.go). A development
build can override it:
go build -ldflags "-X main.licensePublicKeys=<kid>:<base64>" -o wheelhouse-agent .An empty value means no key can verify anything, so every licence is refused. That is the safe failure: a build without keys is a build that cannot be licensed, not one licensed for free.
What the binary can be invoked as#
registerFlags returns a subcommand, and main switches on it. Flags may sit on either
side of the subcommand — splitSubcommand decides whether a bare token is a flag value
or the command itself by consulting the flag table rather than guessing.
| Invocation | What it does |
|---|---|
wheelhouse-agent (no subcommand) | runDaemon(): opens the store, bootstraps the first admin, loads the catalogue, starts the listener and the background goroutines |
wheelhouse-agent version | Prints the version and exits, before any config is read or any secret file is opened |
wheelhouse-agent plan | Prints the operations that would bring the router to the desired-state file, and changes nothing. Exit 2 when it found work |
wheelhouse-agent apply | Stages those operations through a running agent named by --agent-url; --commit --confirm-minutes N commits them instead |
Three refusals in runPlanApply (agent/desired.go) are worth
knowing before you meet them:
applywithout--agent-urlrefuses. The staging area is the running agent's memory, so a one-shot process that filled its own copy would report success and lose the work on exit (ADR-001).--commitwithout--confirm-minutes Nrefuses, in as many words: a mistake should be revertible.applyneeds a usable licence, given as--license-key-filebecause a one-shot command has no store to read one from.planworks without one — it is a read.
There are 51 flags in total. Count them with:
grep -c 'flag\.\(String\|Bool\|Int\|Duration\)Var' agent/main.goThe source map#
Everything is package main in agent/. Open the file whose subject matches; there is
no layer to trace through first.
The router side#
| File | Responsibility |
|---|---|
vyos.go | The client. Form and JSON posts to /retrieve, /show, /configure, /config-file, /image; per-endpoint timeout tiers; turning a 200 carrying an error field into a real error |
opmode.go | The op-mode show reads the product depends on, wrapped once each |
parse.go | VyOS answers op-mode calls with fixed-width text tables; this is what turns them into structures |
cache.go | The read-through cache, its two freshness classes, and the generation counter that keeps a slow fetch from re-poisoning it |
primer.go | The warm-up: at start, and after every commit |
The configuration plane#
| File | Responsibility |
|---|---|
staging.go | The working set of set and delete operations |
desired.go | The desired-state document: format sniffing, flattening, diffing against the tree |
reconcile.go | The optional loop that keeps the router aligned with that file |
parity.go | Power, images, and the other operations that are not configuration changes and so are not staged |
The features#
| File | Responsibility |
|---|---|
wan.go | Uplinks. VyOS has no gateway object, so an uplink is whatever carries a default route, and there are five ways to get one |
ids.go | Suricata, driven entirely through service suricata so it diffs, commits and rolls back |
gaps.go | VRRP and conntrack sync, including what to report when keepalived is not running |
apps.go | App lifecycle: install planning, health, updates, the two-commit restart |
catalog.go | The catalogue types, the embedded catalog.json, install planning and hint expansion |
fleet.go | The router list and its concurrent reads |
Identity, safety and observation#
| File | Responsibility |
|---|---|
auth.go | Argon2id, TOTP, tokens, roles |
authhttp.go | Login, sessions, CSRF, and readOnly / writeable / adminOnly |
admin.go | User and token administration, and the refusals that stop an admin leaving the router with no way in |
oidc.go | Single sign-on as a relying party (ADR-002) |
license.go | Token verification, the daily refresh, and the write-plane gate |
security.go | Body limits, secret redaction, rate limiting, response headers, request metrics |
store.go | state.json, its backup generation, and the append-only audit.jsonl that rotates beside it |
tls.go | Certificates, including generating a self-signed one into the data directory |
metrics.go | A hand-written Prometheus exposition, so the agent keeps its short dependency list |
lock_unix.go / lock_other.go | An advisory lock on the data directory, built per platform |
What runs beside the listener#
runDaemon starts four goroutines and no more:
| Goroutine | Source | Always on? |
|---|---|---|
| Interface history sampler | history.Run in agent/main.go | Yes |
| Licence refresher | runLicenseRefresher in agent/license.go | Yes; does nothing when --license-server is empty |
| Cache primer | runPrimer in agent/primer.go | Unless --demo |
| Reconcile loop | reconciles.Run in agent/reconcile.go | Only with --reconcile-file |
On SIGTERM the server drains in-flight requests for up to 15 seconds rather than
cutting an operator off mid-commit, and the unit gives it 20
(TimeoutStopSec=20).
Things that will surprise you once#
/healthis unauthenticated and carries only{"status":"ok"}. The version used to be there; handing an anonymous caller the exact build to look up was closed deliberately.GET /api/versionanswers for anyone signed in, andwheelhouse-agent versionanswers on the box.- A secret file that is group- or world-readable is refused. Modes are checked, not assumed.
- A second agent against the same data directory will not start. The advisory lock
is what makes
applysafe next to a daemon. - Two agents, one router, is not a supported shape. Nothing enforces it; the staging area is per-process.
See also#
- Architecture — where the agent sits, and what it is allowed to touch.
- Adding an endpoint — the practical version of the wrapper table.
- The test suites —
go vetandgo test -race, and what they cover. - Building the package — where the binary goes next.
- Architecture decision records.
wheelhouse-agent· Every flag — the subcommands and all 51 flags, written for an operator.- Licence keys and states — the token shape
main.licensePublicKeysverifies. - What the licence gates — what that verification ends up deciding.
- What the agent asks the router — why
cache.goandprimer.goare shaped the way they are.
Checked against#
agent/main.go ·
agent/license.go ·
agent/vyos.go ·
agent/cache.go ·
agent/primer.go ·
agent/staging.go ·
agent/desired.go ·
agent/reconcile.go ·
agent/store.go ·
agent/security.go ·
agent/lock_unix.go ·
agent/go.mod ·
packaging/wheelhouse-agent.service ·
.forgejo/workflows/ci.yml