feat(inventory): one service inventory drives planner, allow-lists, and seams
rank 3. shared/services.yml is the single source of truth for every deployable service (hosts, unit dirs, units, path claims, tests, scrape/ dns/probe facts, aliases, stacks); scripts/service-map.py is its only reader (hosts/services/allows/plan/seams/owner/units/stack/get/render/ check). deploy-plan.py becomes a thin CLI wrapper; both --service allow-lists (deploy.sh, deploy-remote.sh) and the changed-test mapper are the inventory. Selection semantics: host scoping (a host's tree selects only its own services), the quadlet netns/network coupling graph resolved across directories (gluetun's riders live in their own dirs), stacks, and canonical restart order. Against the pinned old-planner fixture: 921 raw deltas reduced to 17, all intentional (anubis now maps; cross-host paths select nothing; the retired qwen-sharp template plans nothing and the live froggeric template plans frank-archon; dankserver/romm/romm.container, missed by the old filename rule, plans the romm trio). The fixture is regenerated as the new pin. Integrity gates: schema, unit claims, quadlet-file ownership, path-glob liveness, Pi-hole hosts / Prometheus jobs / DNS / homepage probes drift. tests/service_inventory.bats pins all of it plus the plan fixture and the deploy-plan CLI. Allow-list grep seams in hermes_webui/observability_quadlet become behavioral allows calls.
This commit is contained in:
@@ -0,0 +1,98 @@
|
||||
---
|
||||
type: reference
|
||||
title: Service Inventory
|
||||
description: shared/services.yml is the one inventory of deployable services; scripts/service-map.py is its only reader. The planner, both deploy allow-lists, the changed-test seam mapper, the Pi-hole hosts render, the Prometheus scrape facts, and the homepage probes all derive from it.
|
||||
tags: [deployment, inventory, planner, quadlet, prometheus, pihole, homepage]
|
||||
timestamp: 2026-10-13T00:00:00-07:00
|
||||
---
|
||||
|
||||
# Service Inventory
|
||||
|
||||
Before 2026-10-13 the same facts lived in four hand-maintained lists — the
|
||||
planner's path rules (`deploy-plan.py`), the `--service` allow-list in
|
||||
`deploy.sh`, the near-copy in `deploy-remote.sh`, and the changed-test mapper
|
||||
(`test-changed.sh`) — plus per-fact copies in `prometheus.yml`,
|
||||
`addn-hosts`, and `services.json`. They drifted on every swap (the planner
|
||||
still mapped the retired `qwen-sharp` template to frank-dragoon; the allow-list
|
||||
still listed the *arr stack under Dankserver after it moved to FrankBook-Pro).
|
||||
Now there is **one inventory and one reader**:
|
||||
|
||||
- `shared/services.yml` — the data: every deployable service, its hosts, unit
|
||||
dir(s), unit files, path claims, tests, scrape/DNS/probe facts, aliases.
|
||||
- `scripts/service-map.py` — the only reader. CLI subcommands:
|
||||
`hosts`, `services`, `allows`, `plan`, `seams`, `owner`, `units`, `stack`,
|
||||
`get`, `render`, `check`.
|
||||
|
||||
## 1. Consumers
|
||||
|
||||
|Consumer|Call|
|
||||
|---|---|
|
||||
|`scripts/deploy-plan.py`|thin wrapper → `plan --host TARGET [paths...]` (keeps the `--repo/--base/--head` and `--paths` CLIs)|
|
||||
|`scripts/deploy.sh` (`--service` gate)|`allows --host T --service S`|
|
||||
|`scripts/deploy-remote.sh` (`service_is_allowed`)|same|
|
||||
|`scripts/test-changed.sh`|`seams [paths...]` — a service's declared `tests` are its seams|
|
||||
|`dankclaw/pihole/addn-hosts`|`render pihole-hosts` — the tracked file is a rendered artifact|
|
||||
|Prometheus scrape jobs, LAN DNS, homepage probes|`check` compares the tracked configs against the inventory's `scrape`/`dns`/`probe` fields (drift gate)|
|
||||
|
||||
Selection rules the engine encodes (see `tests/fixtures/service-plan-golden.tsv`,
|
||||
the pinned characterization of every tracked path × every target):
|
||||
|
||||
- **Host scoping** — a host's top-level directory can only select that host's
|
||||
services. `frankbook-pro/**` plans nothing on Dankserver (the old planner
|
||||
had a stale Dankserver *arr list).
|
||||
- **Quadlet coupling graph** — `Network=x.container` rides x's netns;
|
||||
`Network=y.network` joins a shared network. Changing the parent (or the
|
||||
network file) selects every rider transitively, in their own directories
|
||||
(`dankclaw/searxng/quadlet/gluetun.container` → gluetun + searxng + marreta;
|
||||
`frankbook-pro/gluetun/quadlet/gluetun.container` → the whole arr stack).
|
||||
- **Stacks** — a service's `stack` members deploy and restart as one handler
|
||||
(romm trio, hermes trio).
|
||||
- **Retirement** — removing a service from `services.yml` removes it from the
|
||||
planner, both allow-lists, and the seam mapper in one data edit; a pending
|
||||
deploy-state entry for it is auto-triaged (see [[Deploy State Triage]]).
|
||||
|
||||
## 2. Integrity gates (`check`)
|
||||
|
||||
`python3 scripts/service-map.py check` runs, and
|
||||
`tests/service_inventory.bats` pins:
|
||||
|
||||
- **schema** — required fields per entry, known keys only;
|
||||
- **units** — every unit file named by an entry exists under its `unit_dir`
|
||||
on the right host; each unit name is claimed by exactly one service per host;
|
||||
- **ownership** — every tracked quadlet `.container`/`.network` file is claimed
|
||||
by exactly one service (trash excluded);
|
||||
- **paths** — every claimed `files`/`dirs`/`retires` glob matches ≥1 tracked file;
|
||||
- **compose** — the container names in each host's `compose.yaml` have exactly
|
||||
one quadlet owner (the [[Quadlet Lifecycle]] two-owners trap);
|
||||
- **drift** — `addn-hosts` equals its render; the Prometheus job set, the DNS
|
||||
names, and the homepage probe ports equal the inventory's fields.
|
||||
|
||||
## 3. Adding, moving, retiring a service
|
||||
|
||||
- **Add**: append an entry (name, hosts, kind, unit_dir, units, paths, tests,
|
||||
…), add the quadlet file, run `check` — `plan`/`allows`/`seams` pick it up
|
||||
with zero script edits.
|
||||
- **Move between hosts**: edit `hosts` (and per-host `unit_dir`/`probe` maps).
|
||||
- **Retire**: delete the entry, move files to `.AGENTTRASH/` (see
|
||||
[[Testing Policy]] for the seam the mapper applies to trash paths).
|
||||
|
||||
Field notes: `alias` carries deployable names that differ from the service
|
||||
name (`calibre-web` deploys the unit `calibre-server`); `probe` for multi-host
|
||||
services is a `{host: port}` map; `dependents` names services whose config
|
||||
embeds a reference to this one (config-only coupling — a `.env` edit selects
|
||||
both, a unit edit selects the graph closure).
|
||||
|
||||
## 4. Known cutover deltas (2026-10-13)
|
||||
|
||||
The 17 deltas against the old planner are all in these classes: anubis paths
|
||||
now select `anubis` (the old planner had no rule at all); cross-host paths
|
||||
select nothing; `scripts/qwen-sharp-v22.4.0.jinja` (retired template) plans
|
||||
nothing while `scripts/qwen38-froggeric-v22.5.jinja` (the live frank-archon
|
||||
template, previously unmapped) plans frank-archon;
|
||||
`dankserver/romm/romm.container` (top-level quadlet dir the old unit-file rule
|
||||
missed) now plans the romm trio.
|
||||
|
||||
---
|
||||
|
||||
Related: [[Deployment Flow]], [[Testing Policy]], [[Quadlet Lifecycle]],
|
||||
[[Container Network Namespaces]], [[Deploy State Triage]]
|
||||
@@ -28,6 +28,7 @@ covers both workloads).
|
||||
- [[Network Architecture]] — Caddy reverse proxy, hostname routing, Gluetun VPN path
|
||||
- [[Storage Policy]] — bind mounts, data locations, ownership
|
||||
- [[Deployment Flow]] — service-scoped git deploy, target map, and health checks
|
||||
- [[Service Inventory]] — shared/services.yml, the one inventory of deployable services, and service-map.py
|
||||
- [[Testing Policy]] — the test seams and how to run them
|
||||
- [[E2E Suite]] — the Playwright suite that drives the live guard chain
|
||||
- [[Container Network Namespaces]] — which local services are addressable from each Podman network namespace on Dankclaw
|
||||
|
||||
Reference in New Issue
Block a user