Anyone who has maintained a Nagios estate past a few hundred hosts knows
the failure this page is about: use chains four templates deep, a
directive misspelled in a way Nagios accepts and silently ignores, and no
way to know a config is broken until -v catches it, or does not. jaque's
config is CUE: a typed language whose values unify instead of inheriting,
with the schema in the language rather than beside it. This page states
the problem, what CUE replaces it with, and the two distinct points at
which a jaque config can fail.
1. The problem: template inheritance without a type system
Nagios configuration is templates (use) resolved by an ad hoc positional
algorithm: ordered multiple inheritance, +value to append, null to
unset, register 0 for abstract templates. It works, and it is untyped.
A misspelled directive is either ignored or a runtime surprise, and there
is no way to ask whether a config is valid without loading it into the
daemon that will act on it.
2. What CUE gives instead
CUE is built around unification rather than inheritance. Two partial
definitions of the same value merge; they do not override each other in a
resolution order you have to remember. For "shared defaults, per-host
overrides" that is a closer fit than use chains ever were, and it brings
three things with it.
A real schema. jaque defines #Host, #Service, #Check and the rest as
closed structs, so an unknown field such as chekc_interval is a build
error naming the field, not a directive quietly dropped.
Validation as part of the language. Type constraints (string & !=""),
enumerations (disjunctions shaped like
"OK" | "WARNING" | "CRITICAL" | "UNKNOWN") and numeric ranges live in
the schema itself, not in a linter bolted on after the fact.
Diffable, GitOps-friendly text. A CUE file is text you git diff, review
and merge. Nothing is generated by a UI that only the UI can read back.
3. Two validation phases
A config fails in two different places, and the errors differ.
3.1 Constraints, at build
CUE evaluates the object graph against #Schema: types, required fields,
closed-struct field names, and disjunction shape, where a check's type
selects which of #TCPCheck, #HTTPCheck and the rest applies. A typo or
a missing address is caught here, before the graph is fully concrete.
3.2 Concretion, at load
A config that satisfies every constraint can still be incomplete: CUE lets
a value remain an open disjunction or an unresolved reference. Running
jaque -config x.cue forces full concretion at load; anything still
ambiguous fails there, with the field's path in the error.
In practice: run jaque -config yourfile.cue and read the error. It names
the field, never only "config invalid".
4. 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 is not inherited from; it is unified with. Two devices on
that template cannot drift into "which one applied the override" the way
a use chain can. Anchoring on #Host matters: a bare
#CastDevice: {...} is implicitly closed and would reject address and
check as unknown fields at the use site, while anchoring on the schema
keeps it open exactly as the schema is.
CUE in practice is the full walkthrough.
5. The decision, and its reversal criterion
CUE was not a settled bet. ADR-003 set an explicit reversal criterion: the Phase 0 spike had to model three hosts and ten services with a shared template and one override, loaded and validated in-process, or the decision reverted to YAML plus JSON Schema. It passed, and CUE is what runs today. A config language that could not clear that bar would have been replaced, not defended.