design.md, research.md (new), brief.md and requirements.md (updated during spec authoring: Req 1.6 resolved/superseded by bundling, Req 5.5 added, Req 4.3/4.4 extended for limits.yaml) had been sitting uncommitted since before implementation began -- the entire 18-task implementation was built against these documents without them ever landing in git. src/configs/*.yaml + schema/*.schema.json: the example config directory and bundled JSON schemas most of the test suite reads as "real repo fixtures" (test_config_build.py, test_mapping.py, test_validation.py, etc.) were likewise never committed -- those tests would silently skip (skipif guards) rather than run for anyone else checking out this branch.
42 KiB
Brief: admin-config-tool
Problem
XCE operators have no tool to build and deploy cluster configuration. Currently:
- Config authoring: YAML config files (
configs/hosts.yaml,queues.yaml,resources.yaml,users.yaml,limits.yaml,xce.yaml,scaler.yaml) exist as documentation examples with JSON schemas for validation (configs/schema/*.schema.json), but no tool compiles them into theClusterConfigCap'n Proto binary that the controller expects. - Config deployment:
OP_upgradeConfigexists on the controller side (config_repl.c:1591do_upgrade_config_with_bytes()— receives binary blob, verifies SHA-256, saves, pushes to standbys, awaits majority ACK, signals restart), but no CLI sends it. Today: manual scp + restart.
End users use xcecli (Go/Cobra → REST → apiserver → CGO → controller) for non-admin operations. There is no admin-facing tool.
Who: XCE cluster operators. Pain: manual config management, error-prone YAML→binary translation, no primary auto-discovery.
Current State
| What | Where | Status |
|---|---|---|
ClusterConfig capnp struct |
src/schema/controller.capnp:694-708 |
13 fields (@0-@12), wire-ready — down from 21 after clusterconfig-schema-split removed port/candidates/hash-size/xstrings |
| YAML config examples | configs/{hosts,queues,resources,users,limits,xce,scaler}.yaml |
Human-authored, NOT loaded by any code |
| JSON schemas | configs/schema/*.schema.json |
JSON Schema 2020-12 |
OP_upgradeConfig handler |
config_repl.c:1591 do_upgrade_config_with_bytes() |
Receives binary, verifies, pushes, awaits majority |
UpgradeConfigRequest/Reply |
controller.capnp:792-797 |
Defined in schema |
| libxce upgrade API | request.c:1046 has extractors only |
No send function |
| Controller wire protocol | TCP+TLS, capnp-encoded Request/Reply, same CA as controller-to-controller |
Exists, used by all C daemons |
Key gap: YAML → ClusterConfig binary tool does not exist. Upload CLI does not exist.
Desired Outcome
Single xceadmin Python CLI that operators use for cluster configuration management:
# Build: auto-discover YAML files + schemas from standard directory layout
xceadmin config build configs/ # default output: cluster.bin
xceadmin config build configs/ -o cluster.bin # explicit output
xceadmin config build configs/ -o - # stdout (for piping)
# Push: zero-config dev (same as xce_test), single --controller for production
xceadmin config push cluster.bin # default: localhost:9991
xceadmin config push --controller ctrl-1:9991 cluster.bin # production: any one controller
Standard config directory layout (auto-discovered):
configs/
├── hosts.yaml # optional — missing = empty hosts section
├── queues.yaml # optional
├── resources.yaml # optional
├── users.yaml # optional
├── limits.yaml # optional
├── xce.yaml # optional — missing = clusterName/heartbeat/etc. left unset
└── scaler.yaml # optional
No schema/ subdirectory — RESOLVED (2026-07-21): the 7 JSON schemas are bundled with the xceadmin package itself (xcecommon package_data, synced from src/configs/schema/ at build time), not supplied per configs_dir. This mirrors the controller.capnp bundling decision and for the same reason: the schemas are the formal spec of what xcecommon.mapping's field-extraction code actually expects, so they need to stay version-locked to the xceadmin release doing the mapping, not left for an operator to drift out of sync. src/configs/schema/*.schema.json remains in the repo as the source of truth that gets synced — an operator's configs_dir never needs its own schema/ folder, and one present there is not read.
- config build: Scan directory → validate present YAML files against
xceadmin's bundled JSON schemas → map toClusterConfigcapnp struct → serialize tocluster.bin→ output SHA-256. Missing files = empty section (not error). No schema directory to require — bundled, not operator-supplied. - config push: Read
cluster.bin→ connect to controller (defaultlocalhost:9991) → controller reply includes peer list + primary address → follow redirect if needed → send capnpRequest{OP_upgradeConfig, UpgradeConfigRequest{sha256, bytes}}→ receiveReply→ display{versionTag, ackedCount, error}.configGenerationis fully controller-owned: the primary assignsnew_gen = current_gen + 1on receipt and returns it asversionTag; xceadmin never reads, sets, or overrides it — no--generationflag. - Shared package:
xcecommonPython package with capnp schema handling, wire protocol, and config models — reusable by future Python-based tools
Approach
Chosen: Python CLI with single dependency on controller.capnp.
src/schema/controller.capnp ← single source of truth
│
├──▶ c-capnproto → libxce → controller (C daemon, existing)
│
└──▶ pycapnp → xceadmin (Python, new)
│
├── config build: YAML → ClusterConfig → bytes → cluster.bin
└── config push: Request{OP_upgradeConfig, ...} → TLS → Reply
- Language: Python. YAML parsing trivial (
yaml), JSON Schema validation (jsonschema), capnp encoding (pycapnp), CLI (clickorargparse). - Protocol: TCP+TLS directly to controller (NOT through apiserver). Zero-config dev experience — default
localhost:9991, same as xce_test. Single--controllerflag for production override. - Dependencies:
controller.capnpschema file only. Zero CGO, zero libxce. Pure Python, open-source ready. - Auto primary detection: Connect to given controller → controller reply carries peer list + primary address (standby redirect, already in reply schema) → follow redirect to primary → send request. One controller is enough to bootstrap full cluster discovery. Same mechanism xce_test uses
- No CGO: Wire protocol is TLS connect → send one capnp
Request→ receive one capnpReply→ exit. ~150 lines of Python. No iomgr event loop, no vtask thread pool, no persistent connections — those are for daemons, not one-shot CLI tools.
Architecture Decisions (from 2026-07-06 discovery session)
| Decision | Rationale |
|---|---|
| Python over Go | Future open-source release; shared capnp infra with planned Python xcecli rewrite; pip install no compilation |
| Pure Python wire over CGO+libxce | Open-source friendly; no C compilation; wire protocol is one-shot send/receive (~150 lines) |
| Direct controller over apiserver | Admin path needs no RBAC middleware; apiserver may be restarting during config upgrade; controller SSL is same CA |
Single controller.capnp dependency |
Schema IS the protocol contract; versioned; no extra API surface |
Separate xceadmin binary over extending xcecli |
xcecli = user tool (HTTP → apiserver); xceadmin = operator tool (TLS → controller); different audiences, different protocols |
| Auto primary detection | Operator shouldn't need to know which controller is primary; connect to any one, discover peers + primary from reply |
| Config build offline | YAML→binary is pure data transform; no controller connection needed; cluster.bin can be pre-built, reviewed, version-controlled |
xcecli vs xceadmin
| xcecli | xceadmin | |
|---|---|---|
| Audience | End users | Operators |
| Protocol | HTTP/HTTPS (REST, JSON) | TCP+TLS (capnp binary) |
| Target | apiserver | controller (primary) |
| Auth | JWT token (login → apiserver) | None — same zero-config TLS as xce_test |
| Auth middleware | Full: WhitelistURL→WhitelistIP→RateLimit→JWT→CORS→RBAC | None — direct TCP+TLS, same trust model as controller peers |
| Use cases | Submit jobs, list queues, check status | Push config, cluster admin, force snapshot |
| Language | Go/Cobra (current), Python (future) | Python |
Project Decomposition (from 2026-07-06 discovery)
This discovery session (Path E — mixed decomposition) identified 3 workstreams:
| # | Workstream | Type | Language | Dependencies |
|---|---|---|---|---|
| 1 | admin-config-tool | NEW spec | Python | controller.capnp only |
| 2 | config-upgrade-snapshot | EXISTING spec extension | C | entity-bearing-snapshots (blocked upstream) |
| 3 | perf-tuning-extraction | DIRECT work | doc | None |
Dependency flow:
admin-config-tool (standalone, ships first)
│
│ config push triggers controller-side path
▼
config-upgrade-snapshot (controller: replicate → snapshot → restart → activate)
│
│ blocked on
▼
entity-bearing-snapshots (upstream blocker, tracked in roadmap)
Configuration Inclusion Rules
Decided 2026-07-20. Governs ClusterConfig/cluster.bin field classification now and for any new field added in the future — apply both rules to every candidate field; excluding on either is sufficient.
Promoted to project-wide steering:
.kiro/steering/config-design.mdholds the general rule (reusable for worker/scheduler/apiserver shared-config decisions, not justClusterConfig). This section keeps theClusterConfig-specific field classification; see the steering doc for the rule statement and rationale.
- Independence rule: if a parameter can change without needing to stay consistent with the rest of the cluster's config (i.e., changing it on one node doesn't require every other node to match), keep it OUT of
cluster.bin. Example:port— each controller's own listen port is local, other nodes don't need to know or match it via config push. - Frequency rule: if a parameter is expected to change often relative to the rest of cluster config, keep it OUT of
cluster.bin. Example:candidates— adding/removing a standby is a routine operational action; requiring a config build+push for it would make membership changes as heavyweight as a schema change. - Default (both rules fail → include): a parameter that must be identical across every controller instance AND changes rarely belongs IN
cluster.bin. Example:clusterName— divergence between nodes is a correctness bug, and it's set once at bootstrap.
Correction (2026-07-20, after investigating clusterconfig-schema-split): the heartbeatInterval/inactiveHeartbeatCount fields actually govern controller→worker liveness (marking a worker unavailable), not the HA/Raft controller-to-controller election heartbeat as originally assumed here — that election heartbeat has no ClusterConfig field at all (100% hardcoded, and per a bootstrap-availability constraint it never can have one — see .kiro/steering/config-design.md Rule 0). The worker-liveness fields actually pass the independence rule (one controller's worker-timeout tolerance doesn't need to match another's) — they stay IN cluster.bin by deliberate operational-uniformity override, not because they fail the rules. Verdict unchanged; reasoning corrected.
Applying the rules (reference table for future field additions):
| Field | Independence rule | Frequency rule | Verdict |
|---|---|---|---|
clusterName |
fails (must match cluster-wide) | fails (rarely changes) | IN |
| heartbeat/inactive-count (worker liveness) | passes (per-controller worker-timeout tolerance, no cross-node dependency) | — | IN (operational-uniformity override — see correction above) |
port |
passes (per-node, no cross-node dependency) | — | OUT |
candidates |
passes (adding a standby doesn't require other nodes' config to change) | passes (routine op) | OUT |
jobmgr/workermgr/qmgr/resmgr hash size |
passes (perf tuning, local effect only) | — | OUT |
xstrings hash size |
passes (perf tuning, local effect only) | — | OUT |
limits |
fails (quotas must be enforced identically on every controller) | fails (rarely changes) | IN |
fetchJobInterval |
passes (per-controller scheduler-loop tuning, no cross-node dependency) | — | IN (operational-uniformity override, bundled into xce.yaml with the other cluster-identity/liveness fields rather than split into a separate file) |
netioTimeout |
passes (per-controller network tuning, no cross-node dependency) | — | IN (same override, same file — see fetchJobInterval row) |
hproviders (scaler providers) |
fails (a queue's scaler reference must resolve identically after failover) | fails (rarely changes) | IN |
configGeneration |
N/A — not operator input; controller-assigned on every push | N/A | OUT (different reason: not a config value at all, it's protocol state) |
When a new field is proposed for ClusterConfig in the future, run it through rules 1–2 before adding it to xceadmin's YAML→capnp mapping; document the verdict in this table.
Scope
- In:
xceadmin config build: YAML parsing, JSON Schema validation, capnp ClusterConfig serialization, cluster.bin output with SHA-256.configGeneration,port, and the per-instance perf-tuning fields (manager hash-table sizes,xstrings) are left unset — not xceadmin's to set.xceadmin config push: single controller address (defaultlocalhost:9991), auto primary detection via peer-list-from-reply, TLS+capnp wire protocol, capnp UpgradeConfigRequest/Reply, result display.configGenerationassignment is entirely controller-side (primary assignscurrent + 1, returns it asversionTag) — no client probe, increment, or override flag.xceadmin config push --dry-run: (nice-to-have) validate controller will accept the config without committing itxcecommonshared Python package: capnp schema loading, wire protocol (TLS+capnp framing), config model types- Build system integration: pip-based install (editable for dev, pinned for release), nix dev shell packages, xce-build.sh component target
- Skills integration (see Skill Integration section below):
xce-build: addxceadmincomponent target (Python package build + test)xce-deploy-ha: replace manualha/data/cluster.binprerequisite withxceadmin config buildstep; add--build-configflag for on-the-fly YAML→binary generationxce-test-ha: (future) config push validation round in soak loop — deferred untilconfig-upgrade-snapshotlands on controller side
- Validation: reject configs using reserved name
"lost_and_found"for queue/host (defense-in-depth — controller enforces at runtime via FATAL, tool prevents at authoring time)
- Out:
- Controller-side config replication/snapshot/restart logic (owned by
config-upgrade-snapshot) - Entity-bearing snapshot payload (owned by
entity-bearing-snapshots, upstream blocker) - Performance tuning / per-instance parameter extraction (separate direct work item) —
port, manager hash-table-size fields,xstrings; these stay local to each controller instance, never incluster.bin - xcecli Python rewrite (future, separate spec)
- xce-test-ha config push validation round (deferred until
config-upgrade-snapshotlands — config push to a controller that can't replicate/snapshot is a no-op from the soak's perspective)
- Controller-side config replication/snapshot/restart logic (owned by
Skill Integration
Three existing skills touch cluster.bin — the binary config blob that xceadmin config build produces. Currently cluster.bin is a hand-crafted opaque artifact with no visible provenance. xceadmin replaces that with a reproducible build step.
xce-build — add xceadmin component
New component target for the Python package:
/xce-build xceadmin # pip install -e . (editable, for dev)
/xce-build xceadmin release # pip install . (pinned, for production)
Build path differs from C components: Python, not C. Runs inside nix dev shell (pycapnp needs capnproto C++ runtime from nix). Build output: xceadmin CLI entry point + xcecommon package importable.
This is the blocking integration — other skills can't use xceadmin until this lands.
xce-deploy-ha — generate cluster.bin from YAML
Current flow:
ha/data/cluster.bin (pre-built, manual, opaque) → deploy-ha.sh → controller/ctrl-1/conf/
New flow:
configs/*.yaml → xceadmin config build → cluster.bin → deploy-ha.sh → ...
Changes to the skill:
| What | Before | After |
|---|---|---|
| Prerequisites | cluster.bin must exist under ha/data/ |
cluster.bin built from configs/*.yaml via xceadmin config build |
| Config build step | None (manual) | xceadmin config build configs/ -o ha/data/cluster.bin (auto, or skip if --use-existing-config) |
--build-config flag |
N/A | New flag: force rebuild cluster.bin from YAML before deploy |
deploy-ha.sh gains an optional precondition: if cluster.bin missing or --build-config passed, run xceadmin config build. Otherwise use existing cluster.bin as-is (backward compatible — operators can still hand-craft for custom configs).
xce-test-ha — config push validation (future)
Deferred until config-upgrade-snapshot lands on controller side. When ready:
- New soak round type: kill primary, push new config via
xceadmin config push, verify controller accepted + standbys received - Coverage: exercises
OP_upgradeConfig→ replication → majority ACK path under failover - Verdict: config push failure = FAIL (config consistency is a data-safety property)
Not scoped into admin-config-tool tasks — belongs to config-upgrade-snapshot spec. Listed here for cross-spec visibility.
Integration dependency order
xce-build (+xceadmin) ← THIS SPEC, blocking
│
▼
xce-deploy-ha ← THIS SPEC, immediate after xce-build
│
▼
xce-test-ha ← config-upgrade-snapshot spec (future)
(config push round) blocked on entity-bearing-snapshots
Harness Enhancement
The xceadmin tool is Python — first Python component in a C/Go project. This requires harness changes across the development toolchain.
xce-build.sh — new xceadmin component
Add a new case to src/tools/xce-build.sh:
xceadmin)
echo "=== Building xceadmin Python package ==="
pip install -e . --quiet
echo "=== Running xceadmin tests ==="
python -m pytest tests/ -v
;;
Python path — no meson setup. pip install -e . for dev (editable), pip install . for release. Tests via pytest, not meson test. Runs inside nix dev shell (capnproto C++ runtime needed for pycapnp compile). The release profile uses pip install . without -e.
xce-build SKILL.md — add component + usage note
- Usage line:
Components: all (default), lib, controller, scheduler, worker, apiserver, xcecli, xceadmin - Profile notes:
xceadminignores meson profiles — it always uses pip. The profile arg affects C components only; xceadmin builds regardless of profile. - Examples:
/xce-build xceadmin(editable),/xce-build xceadmin release(pinned)
xce-deploy-ha SKILL.md — config build step
Changes to the skill document:
- Prerequisites: replace "TLS certs and a
cluster.binmust exist underha/data/" with "TLS certs must exist underha/data/.cluster.binis built fromconfigs/*.yamlviaxceadmin config build(or use existing if--use-existing-config)." - New
--build-configflag: forces rebuild ofcluster.binfrom YAML before deploy. Without it, uses existingcluster.binas-is (backward compatible). - New step (between resolve paths and stop running): optional
xceadmin config build configs/ -o ha/data/cluster.binif--build-configpassed orcluster.binmissing. - Troubleshooting: add "
pycapnpimport error" → run/xce-build xceadminfirst.
settings.local.json — Python permissions
Add Bash permission allowlist entries for xceadmin development:
"Bash(pip install *)",
"Bash(python -m pytest *)",
"Bash(xceadmin config build *)",
"Bash(xceadmin config push *)",
"Bash(ruff format *)",
"Bash(ruff check *)"
PostToolUse hooks — Python formatting + lint
Add a ruff format hook for Python file edits (settings.json):
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "command",
"command": "[[ \"$FILE_PATH\" == *.py ]] && ruff format --quiet \"$FILE_PATH\" || true",
"timeout": 5
}
]
}
Add a ruff check hook for lint feedback:
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "command",
"command": "[[ \"$FILE_PATH\" == *.py ]] && ruff check --quiet \"$FILE_PATH\" || true",
"timeout": 5
}
]
}
PreToolUse hook — YAML schema guard (nice-to-have)
Validate config YAML files against JSON schemas before accepting edits. Prevents commits of invalid config:
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "command",
"command": "[[ \"$FILE_PATH\" == configs/*.yaml ]] && xceadmin config validate \"$FILE_PATH\" || true",
"timeout": 5
}
]
}
This requires xceadmin config validate subcommand (nice-to-have: validates a single YAML file against its schema, no binary output). Deferred to a follow-up task after core config build lands.
/xce-admin skill (deferred)
Thin skill wrapper around xceadmin config build/push for agent-driven workflows. Not needed for spec implementation — CLI is sufficient. Deferred until CLI is stable and operator workflows are validated.
Harness change summary
| # | What | Where | Priority | Phase |
|---|---|---|---|---|
| 1 | xceadmin case in build script |
src/tools/xce-build.sh |
Blocking | Implementation |
| 2 | Component list + pip note | .claude/skills/xce-build/SKILL.md |
Blocking | Implementation |
| 3 | --build-config + config build step |
.claude/skills/xce-deploy-ha/SKILL.md |
Blocking | Implementation |
| 4 | pip/pytest/xceadmin permissions | .claude/settings.local.json |
Blocking | Implementation |
| 5 | ruff format hook on .py edits |
.claude/settings.json |
Nice-to-have | Implementation |
| 6 | YAML schema guard on config edits | .claude/settings.json |
Nice-to-have | Follow-up |
| 7 | /xce-admin skill wrapper |
.claude/skills/xce-admin/SKILL.md |
Deferred | Post-spec |
Boundary Candidates
xceadminCLI — operator-facing commands, argument parsing, output formattingxcecommonPython package — shared capnp schema loading, wire protocol (TLS+capnp), config model types. Reusable by future Python xcecli.config-buildmodule — YAML→ClusterConfig mapping, JSON Schema validation, serializationconfig-pushmodule — primary auto-detection, TLS+capnp send/receive, result parsing
Upstream / Downstream
- Upstream:
controller.capnpschema (source of truth for ClusterConfig, Request/Reply, OP codes) - Downstream: Controller
OP_upgradeConfighandler (config_repl.c— already exists) - Consumed by:
config-upgrade-snapshotspec — xceadmin is the trigger; controller-side handles replication/snapshot/restartxce-buildskill — addsxceadmincomponent target (blocking integration)xce-deploy-haskill —xceadmin config buildreplaces manualcluster.binprerequisitexce-test-haskill — (future) config push validation round, deferred until config-upgrade-snapshot lands
- Future consumer: Python xcecli rewrite (imports
xcecommonfor capnp schema types)
Constraints
- Python ≥ 3.10 (match system Python, avoid bleeding-edge features)
- pycapnp requires capnproto C++ runtime (
libcapnp-dev) — separate from project's c-capnproto (C bindings). Both capnp runtimes can coexist; binary format is standardized. - Zero-config TLS — same dev experience as xce_test: default
localhost:9991, no cert flags needed. Controller peer trust model (X.509 CN validation viavalidate_host()— tracked in roadmap hardening). - No dependency on a running apiserver — admin tool connects directly to controller. Config deployment works even when apiserver is down or restarting.
- Open-source readiness — pure Python,
pip installfriendly, no compilation step, clear single-schema dependency.
Nix Environment
xceadmin is the first Python component in the XCE project. The nix dev shell currently provides C/C++ tooling only — zero Python packages. All needed Python packages are available in nixpkgs.
Packages to add
Current src/nixSupport/xce.nix nativeBuildInputs already includes capnproto (line 21) — the C++ runtime that pycapnp wraps. Need to add:
| Package | Nix attr | Purpose | Size |
|---|---|---|---|
| Python 3 | python3 |
Interpreter (≥ 3.10) | ~50MB |
| pycapnp | python3Packages.pycapnp |
capnp schema → Python types | small |
| PyYAML | python3Packages.pyyaml |
YAML config parsing | small |
| jsonschema | python3Packages.jsonschema |
JSON Schema validation | small |
| click | python3Packages.click |
CLI argument parsing | small |
| pytest | python3Packages.pytest |
Test framework | small |
| ruff | python3Packages.ruff |
Python formatter + linter | ~10MB |
All confirmed available in nixpkgs (unstable): pycapnp 2.2.3, pyyaml 6.0.3, jsonschema 4.26.0, click 8.3.1, pytest 9.0.3, ruff 0.15.20.
Where to add
Two options:
Option A: Add to xce.nix (recommended). Every dev gets Python toolchain. Simpler — one flake, one shell. Python deps are ~60MB total, negligible vs current C toolchain.
Option B: Separate xceadmin.nix. Lighter shell for C-only devs. More maintenance — two nix files, two shells, cross-component workflows need manual env switching.
Recommend Option A — xceadmin is part of the standard toolchain (like xcecli), not an optional extra. All devs should be able to xceadmin config build without switching shells.
New nativeBuildInputs block (addition to xce.nix)
# Python — for xceadmin (admin config tool)
python3
python3Packages.pycapnp
python3Packages.pyyaml
python3Packages.jsonschema
python3Packages.click
python3Packages.pytest
python3Packages.ruff
pycapnp build note
pycapnp wraps the capnproto C++ library at compile time (pip install compiles a C extension). Nix's python3Packages.pycapnp handles this correctly — it links against nix-provided capnproto. No additional libcapnp-dev or system package needed. This is one of the advantages of using nix pycapnp over pip install pycapnp.
CI implications
- CI already uses nix dev shell — adding packages to
xce.nixmakes them available in CI automatically config buildgolden-file tests need pycapnp for capnp encode (can't mock without schema)config pushmock tests need pycapnp + TLS (Pythonsslstdlib, no extra dep)
Testing Strategy
config build (offline — pure data transform)
Golden-file tests (primary). Given a configs/ directory, verify output matches expected:
tests/
├── fixtures/
│ ├── minimal/ # hosts.yaml + queues.yaml only
│ │ ├── configs/
│ │ │ ├── hosts.yaml
│ │ │ └── queues.yaml # no schema/ — validated against xceadmin's bundled schemas
│ │ └── expected.bin # pre-built golden output
│ ├── full/ # all 7 YAML files
│ └── invalid/ # schema violations → expect error
├── test_config_build.py # pytest: build → compare binary, verify SHA-256
└── test_schema_validation.py # pytest: each schema violation case
- Framework:
pytest(standard Python, no deps beyond dev) - Zero external dependencies — no controller, no network
- Pre-build golden files once; commit to repo
- Schema validation: test each error case (missing required field, wrong type, invalid enum, reserved name
lost_and_found)
config push (online — needs controller)
Option A: Mock controller (unit/integration). Python TLS server that speaks enough capnp wire protocol to accept OP_upgradeConfig and return canned UpgradeConfigReply.
Option B: Real controller (E2E). Piggyback on existing /xce-test-ha cluster. After cluster is up, xceadmin config push a known config and verify controller accepts it.
- Option A for dev loop + CI (fast, no cluster setup)
- Option B for acceptance (real SSL, real replication, real controller)
Recommended: Option A for spec tasks. Option B deferred to xce-test-ha integration after config-upgrade-snapshot lands.
CI Integration
config buildtests: run in any Python CI (no C deps needed — pycapnp only for encode, can mock)config pushmock tests: need pycapnp + controller.capnp compiled for Python- Target: 80%+ coverage (project standard)
Open Design Questions
Issues identified during 2026-07-07 review — need resolution before or during requirements generation.
Critical (resolve before requirements)
| # | Question | Why it matters | Candidates |
|---|---|---|---|
| 1 | YAML→capnp mapping incomplete | RESOLVED — all 13 fields accounted for. ClusterConfig is now 13 fields (@0-@12, post-clusterconfig-schema-split), covered by 7 YAML files: resources→resources.yaml, queues→queues.yaml, ugroups→users.yaml, hgroups+hostConfigs→hosts.yaml, limits→limits.yaml (6 fields, 5 files). clusterName/heartbeatInterval/inactiveHeartbeatCount/fetchJobInterval/netioTimeout→xce.yaml (5 fields, 1 file, Req 1.11). hproviders→scaler.yaml (1 field, 1 file, Req 1.12). configGeneration excluded, controller-assigned (1 field, row 2). 6+5+1+1 = 13. |
Closed — no candidates remain |
| 2 | configGeneration | Monotonic counter. RESOLVED: fully controller-owned. Controller auto-increments new_gen = current_gen + 1 on every OP_upgradeConfig receipt (already implemented, config_repl.c) and returns it as versionTag. xceadmin never reads, sets, or overrides it — config build leaves the field unset, config push sends the binary as-is, no --generation flag. Same generation as current = no-op is a controller-internal detail, not something xceadmin needs to detect. |
✅ Decided: controller-only, no client involvement |
| 3 | candidates field | Controller peer list (List(MasterCandidate)). RESOLVED — excluded from configuration (Req 1.10): standby add/remove changes independently and more frequently than the rest of cluster config, so it fails both Configuration Inclusion Rules below. xceadmin never sources it from YAML/CLI, cluster.bin leaves it unset. Open sub-question: how peer membership IS sourced/updated instead (today: CLI args to deploy-ha.sh) — not a cluster.bin field going forward |
(a) keep as deploy-ha.sh CLI args (status quo, out of xceadmin scope), (b) separate xceadmin peer add/remove command (future), (c) controller-side dynamic membership (future, own spec) |
| 4 | YAML key ≠ capnp field name | YAML groupname → capnp GroupConfig.name, YAML exclude_members → capnp exmembers. Every field needs explicit mapping definition |
Write a field-mapping table in design.md; auto-generate from capnp schema annotations if possible |
| 5 | Nested struct defaults | QueueConf has 14 fields — YAML sets ~6. Remaining (attrs, status, schedDelay, resreq, preExec, postExec) need defaults |
Use controller's get_default_conf() values from myconf.c; document which fields are operator-facing vs system-defaulted |
Important (resolve during design)
| # | Question | Why it matters | Candidates |
|---|---|---|---|
| 6 | Wire framing format | Controller expects 8-byte uint64 size prefix (host byte order) + capnp payload. ~5 lines of Python but wrong byte order = silent reject | Document in design.md; mock controller test catches framing errors |
| 7 | TLS cert discovery | ✅ Decided: reuse server-side cert bundle. xceadmin loads xce.crt/xce.key from ha/data/ by default (same files deploy-ha.sh copies for controller-to-controller TLS), overridable via --cert-dir. No separate CA flag, no client cert issuance — same trust model as xce_test. See Requirement 2.3/2.4. |
|
| 8 | capnp schema compilation | ✅ RESOLVED (design phase): no capnp compile -o python codegen step exists — pycapnp loads controller.capnp via runtime dynamic reflection (capnp.load(), DynamicStruct), not static generated code. The only build-time step is a plain file sync (not compilation): src/schema/{controller,c}.capnp and src/configs/schema/*.schema.json are copied into xcecommon's package_data by xce-build.sh's xceadmin) case so both schema types survive pip install . (non-editable), resolved at runtime via importlib.resources. See design.md xcecommon.schema. |
|
| 9 | Python package layout | Where does code live? Affects pip install, import paths, xce-build.sh | (a) src/xceadmin/ alongside C code, (b) tools/xceadmin/ with deploy-ha.sh, (c) top-level xceadmin/ |
| 10 | existing cluster.bin provenance | ha/data/cluster.bin is 8704 bytes, SHA 4986624f..., built June 21 by unknown method. Can't validate xceadmin output against it |
Regenerate from known YAML inputs; use as golden-file test once xceadmin produces identical output for same inputs |
Configuration Lifecycle & Rollback (Future Roadmap)
Source of truth: Git-managed YAML
YAML config files (configs/*.yaml) are the authoritative source of truth. Operators manage them in git — version control, code review, audit trail are all free. cluster.bin is a derived artifact (like a compiled binary), never committed to git.
git tag v1.2.0 → YAML at v1.2.0 → xceadmin config build → cluster.bin (deterministic)
│
xceadmin config push
│
▼
controller cluster
Deterministic builds: same YAML + same xceadmin version → byte-identical cluster.bin. configGeneration is left unset by xceadmin config build and never touched by xceadmin config push — the primary controller alone assigns it on receipt (current + 1), so it plays no part in build determinism. Any git revision can be rebuilt to the exact binary that was (or would have been) pushed. No need to store historical binaries.
What git gives us (free)
| Capability | How |
|---|---|
| Version history | git log configs/ |
| Diff between versions | git diff v1.2.0 v1.3.0 -- configs/ |
| Rollback source | git checkout v1.2.0 -- configs/ && xceadmin config build |
| Audit trail | git blame configs/queues.yaml |
| Code review | PR against configs/ branch |
What still needs building
Git manages the source. These parts need implementation:
| # | Capability | What it does | Owned by |
|---|---|---|---|
| R1 | Binary diff | xceadmin config diff cluster-v1.bin cluster-v2.bin — human-readable diff of two built config binaries (decode both → compare field-by-field → pretty-print changes). Useful for pre-push review: "what will this config change actually do?" |
deferred (2026-07-21) — no spec owner yet |
| R2 | Dry-run push | xceadmin config push --dry-run cluster.bin — validate controller will accept the config (SHA check, version check) without committing. Safety net before real push |
admin-config-tool |
| R3 | Config change snapshot | After config upgrade, take an entity-bearing snapshot of the new configuration state. This anchors the config change in the snapshot chain — without it, rollback has no durable record of what the cluster looked like before/after the change. Controller-side: insert snapshot step into the OP_upgradeConfig pipeline (between step 5 "majority ACK" and step 6 "commit WAL_CONFIG_MARKER" in the current handler). |
config-upgrade-snapshot |
| R4 | Controller version history | Controller tracks last N config versions (tag + SHA + timestamp). xceadmin config history queries it. Needed for: "what config is the cluster running?" and "what was running before the last push?" Depends on R3 (snapshot chain provides durable version records). |
config-upgrade-snapshot |
| R5 | Controller-side rollback | xceadmin config rollback (or xceadmin config push --rollback-to v1.2.0) — push a previous config version. Controller-side concerns: (a) validate rollback target is safe (no running jobs reference deleted queues/hosts), (b) snapshot before rollback for forward-recovery, (c) bump configGeneration even on rollback (monotonic), (d) restart/activate the rolled-back config |
config-upgrade-snapshot |
| R6 | Rollback safety validation | Before rolling back, verify: (a) no running jobs use queues being deleted, (b) no active workers on hosts being removed, (c) resource definitions haven't changed in ways that break in-flight allocations. Controller-side check, client-side initiated | config-upgrade-snapshot |
| R7 | Config activation status | xceadmin config status — show: current config tag, when applied, which nodes have it, any pending restart, snapshot status. Operator visibility into config rollout state |
config-upgrade-snapshot |
| R8 | Config promotion pipeline | Dev → staging → production config promotion. Same YAML source, different --peers/--cluster-name flags at build time. Git branches map to environments |
admin-config-tool (future) |
Controller-side pipeline: current vs target
CURRENT (config_repl.c — exists today):
xceadmin push → SHA verify → decode → gen++ → save → replicate → WAL marker → restart signal
↑
step 8 MISSING
TARGET (after config-upgrade-snapshot):
xceadmin push → SHA verify → decode → gen++ → save → replicate → [SNAPSHOT] → WAL marker → restart signal
↑
R3: entity-bearing
snapshot of new config
The snapshot insertion point matters: it goes AFTER replication (all standbys have the config) but BEFORE the WAL marker commit (so the snapshot is part of the committed state). This ensures a rollback target always has a consistent snapshot to restore from.
Rollback workflow (end-to-end vision)
# 1. Discover what went wrong
xceadmin config diff cluster-current.bin cluster-suspect.bin
# → "queues.normal.priority changed 10→1, queues.express removed"
# 2. Find the last good version
xceadmin config history
# → v1.2.0 (active), v1.1.0 (previous), v1.0.0 (initial)
# 3. Dry-run the rollback
xceadmin config build --git-ref v1.1.0 -o cluster-v1.1.0.bin
xceadmin config push --dry-run cluster-v1.1.0.bin
# → "OK: controller will accept this config (generation 3→4)"
# 4. Execute rollback
xceadmin config push cluster-v1.1.0.bin
# → "Pushed (tag=v1.1.0, SHA=..., acked=3/3). Controller restarting."
# 5. Verify
xceadmin config status
# → "v1.1.0 active on 3/3 nodes"
Rollback safety: what can go wrong
| Scenario | Risk | Mitigation |
|---|---|---|
| Rollback deletes a queue | Running jobs on that queue become unmanageable | R5: controller refuses rollback while jobs reference deleted queues |
| Rollback removes a host | Workers on that host lose config | R5: controller refuses rollback while active workers on removed hosts |
| Rollback changes resource defs | In-flight allocations invalid | Controller fences: drain before rollback, or mark resources "changing" |
| Rollback during network partition | Standbys miss the config push | Majority ACK required (existing OP_upgradeConfig behavior) |
| Rapid rollback→forward→rollback | configGeneration churn, snapshot chain fragmentation | Rate-limit config changes (1 per N seconds); snapshot GC |
What's in-scope for admin-config-tool vs deferred
| Capability | This spec | Why |
|---|---|---|
| YAML source in git | ✅ (documentation) | Free — already how configs/ works |
| Deterministic build | ✅ (core) | Same YAML → same binary (generation not set — controller-owned) |
| Binary diff (R1) | ❌ deferred (2026-07-21) | Descoped from admin-config-tool — pure Python/offline, no controller dependency, but no operator has asked for it yet; revisit as follow-up once config push sees real-world usage |
| Dry-run push (R2) | ✅ (nice-to-have) | Needs ProbeConfigTag RPC (already exists in controller) |
| Config change snapshot (R3) | ❌ deferred | Controller-side — insert entity-bearing snapshot into OP_upgradeConfig pipeline; blocked on entity-bearing-snapshots |
| Version history (R4) | ❌ deferred | Controller-side — depends on R3 snapshot chain |
| Rollback (R5) | ❌ deferred | Controller-side — needs snapshot chain + restart |
| Safety validation (R6) | ❌ deferred | Controller-side — needs job/worker state queries |
| Config status (R7) | ❌ deferred | Controller-side — needs cluster-wide state tracking |
| Promotion pipeline (R8) | ❌ deferred | Wraps git branches + xceadmin build — future enhancement |
Existing Spec Touchpoints
- Extends:
config-upgrade-snapshot(provides the client-side trigger that config-upgrade-snapshot's controller-side path responds to; R3 config-change-snapshot is the missing controller-side step — insert entity-bearing snapshot between replication and WAL marker commit in theOP_upgradeConfigpipeline) - Adjacent:
ha-term-safety(the WAL_CONFIG_MARKER this tool's upload commits anchors on the Raft log via ha-term-safety's term-index infrastructure) - Upstream blocker:
entity-bearing-snapshots— must land before R3 (config change snapshot) and R4 (version history) can be implemented - Blocks: nothing — xceadmin ships independently; the current controller pipeline (SHA verify → decode → gen++ → save → replicate → WAL marker → restart) already works without the snapshot step
- Future: configuration lifecycle (R1–R8 above) — split across admin-config-tool (R2, R8) and config-upgrade-snapshot (R3–R7); R1 (binary diff) deferred, no spec owner yet