Why CUE
If you’ve maintained a Nagios estate past a few hundred hosts, you already
know the pain this page is about: use chains four templates deep, a
typo in a directive that Nagios accepts and just silently ignores, and no
way to know a config is broken until -v catches it (or doesn’t).
The problem: template inheritance without a type system
Section titled “The problem: template inheritance without a type system”Nagios configuration is templates (use) resolved by an ad hoc,
positional inheritance algorithm: ordered multiple inheritance, +value
to append, null to unset, register 0 for abstract templates. It works,
but it’s untyped — a misspelled directive is either silently ignored or a
runtime surprise, and there’s no way to ask “is this config valid?”
without loading it into the running daemon.
What CUE gives you instead
Section titled “What CUE gives you instead”CUE is a typed configuration language built around unification rather
than inheritance. Two partial definitions of the same value merge — they
don’t override each other in some template resolution order. That’s a
better fit for “shared defaults, per-host overrides” than use chains
ever were, and it comes with:
- A real schema. jaque’s config schema defines
#Host,#Service,#Checkand friends as closed structs — an unknown field likechekc_interval(typo) is a build error naming the exact field, not a directive Nagios quietly drops. - Validation as part of the language, not a separate linter bolted on
after the fact. Type constraints (
string & !=""), enumerations ("OK" | "WARNING" | "CRITICAL" | "UNKNOWN"-shaped disjunctions), and numeric ranges live right in the schema. - Diffable, GitOps-friendly text. A CUE file is still just text you
git diff, review, and merge — nothing is generated by a UI that only the UI can read back correctly.
Two validation phases
Section titled “Two validation phases”Config validation happens in two passes, and they fail differently:
- Constraints, at build. CUE evaluates the object graph against
#Schema— types, required fields, closed-struct field names, disjunction shape (a check’stypepicks which of#TCPCheck,#HTTPCheck, … applies). This is where a typo or a missingaddressgets caught, before the config graph is even fully concrete. - Concretion, at validate. Even a config that satisfies every
constraint can still be incomplete — CUE lets you leave values as
open disjunctions or references that never got resolved to a concrete
value.
jaque -config x.cuecalling intoconfig.NewConfigforces full concretion; anything still ambiguous at that point fails there, with the field’s path in the error.
Practically: run jaque -config yourfile.cue and read the error. It names
the field, not just “config invalid.”
What this replaces, concretely
Section titled “What this replaces, concretely”The Nagios idiom of a generic-host template three levels of use deep
becomes a CUE template you anchor structs on:
#CastDevice: #Host & { parents: ["router"] check: type: "tcp"}
hosts: chromecast: #CastDevice & { address: "192.168.1.200" check: address: "192.168.1.200:8009"}#CastDevice isn’t inherited from — it’s unified with. Two Cast devices
sharing that template can’t drift into “wait, which one applied the
override” the way a use chain can. Anchoring on #Host matters: a bare
#CastDevice: {...} template would be implicitly closed and reject
address/check as unknown fields at the use site — anchoring on the
schema keeps it open the way the schema itself is. See CUE in
practice for a full config walkthrough.
CUE as the config language passed an explicit reversal criterion, not a settled bet made lightly: ADR-003’s Phase 0 spike had to model 3 hosts / 10 services with a shared template and one override, loaded and validated from Go, or the decision reverted to YAML + JSON Schema. It passed, and CUE is the one in place today.