- ADR: ADR 0117
- Title: Persist a machine-usable DAG of service/host/network/cert dependencies in Postgres; expose traversal API for blast-radius, health propagation, and incident correlation used by triage and risk scoring
- Status: live_applied
- Branch:
codex/ws-0117-live-apply - Worktree:
../proxmox-host_server_ws-0117-live-apply - Owner: codex
- Depends On:
adr-0048-command-catalog,adr-0054-netbox-topology,adr-0058-nats-event-bus,adr-0075-service-capability-catalog,adr-0092-platform-api-gateway,adr-0098-postgres-ha,adr-0113-world-state-materializer - Conflicts With:
adr-0113-world-state-materializer(both read from NetBox; coordinate on refresh timing to avoid stampede) - Shared Surfaces:
platform/graph/,graph.nodesandgraph.edgesPostgres schema,config/dependency-graph.yaml,config/windmill/scripts/graph/,/v1/graph/*API gateway endpoints
- create Postgres migration
migrations/0012_graph_schema.sql—graph.nodesandgraph.edgestables with indexes from ADR 0117 - create
config/dependency-graph.yaml— initial manual edge declarations for all platform services (netbox→postgres, keycloak→postgres, windmill→postgres, mattermost→postgres, grafana→loki, grafana→prometheus, all services→step-ca, all services→docker-runtime) - create
platform/graph/__init__.py - create
platform/graph/client.py—DependencyGraphClientwithdescendants(),ancestors(),path(),neighbourhood(),health_propagation()methods using recursive CTEs - create
config/windmill/scripts/graph/import-from-netbox.py— Windmill workflow: imports device→rack, IP, VLAN edges from NetBox world-state snapshot - create
config/windmill/scripts/graph/import-from-catalog.py— Windmill workflow: importsdepends_onedges fromconfig/workflow-catalog.json - create
config/windmill/scripts/graph/propagate-health.py— Windmill workflow subscribed toplatform.world_state.refreshedonservice_healthsurface; runs health propagation and emitsderived_health_degradedNATS events - register
/v1/graph/*routes on the platform API gateway (ADR 0092):GET /v1/graph/nodes,GET /v1/graph/nodes/{id}/descendants,GET /v1/graph/nodes/{id}/ancestors,GET /v1/graph/nodes/{id}/health,GET /v1/graph/path - write
tests/unit/test_graph_client.py— test recursive traversal, cycle detection, empty graph, single-node graph
- Code-level or build-time dependency analysis
- Replacing NetBox as the topology source of truth
- Real-time edge updates (graph is rebuilt from sources on each refresh cycle, not updated incrementally)
migrations/0012_graph_schema.sqlconfig/dependency-graph.yamlplatform/graph/__init__.pyplatform/graph/client.pyconfig/windmill/scripts/graph/import-from-netbox.pyconfig/windmill/scripts/graph/import-from-catalog.pyconfig/windmill/scripts/graph/propagate-health.pydocs/adr/0117-service-dependency-graph-runtime.mddocs/workstreams/adr-0117-dependency-graph-runtime.md
graph.nodescontains rows for all platform services, VMs, and hostsgraph.edgescontains at minimum alldepends_onedges declared inconfig/dependency-graph.yamlDependencyGraphClient().descendants("service:postgres")returns the correct set of dependent servicesGET /v1/graph/nodes/service:postgres/descendantsvia the API gateway returns the same set- Stopping the health probe for a service causes a
derived_health_degradedNATS event for its dependents within 60 seconds
- Run
pytest tests/unit/test_graph_client.py -v→ all tests pass - Confirm
SELECT count(*) FROM graph.nodes;returns at least 15 rows after the import workflows run - Call
DependencyGraphClient().descendants("service:postgres")→ verify keycloak, netbox, windmill, mattermost all appear - Simulate a health probe failure for postgres (set
available: falsein a test snapshot) → confirmderived_health_degradedNATS events arrive for downstream services within the propagation window
- Unit tests pass including cycle-detection test
- Graph populated for all current platform services
- Health propagation verified end-to-end
- API gateway endpoints responding with correct data
- Risk scorer (ADR 0116)
downstream_countstub replaced with realDependencyGraphClient().descendants()call
- Repository implementation remains tracked from repo release
0.117.0. - Live apply completed from the separate
codex/ws-0117-live-applybranch on 2026-03-26 and is verified against platform version0.130.20. - The replay repaired the Windmill graph runtime's dependency bootstrap path, exported the graph DSNs into the managed runtime environment, restored graph-schema grants for
windmill_admin, and refreshes the ADR 0113world_state.current_viewmaterialized view during converge so the graph import workflows can run against populated world-state data.
- Focused repository validation passed with the ADR 0117 pytest suite (
29 passed),make syntax-check-windmill,make syntax-check-api-gateway, andscripts/validate_repository_data_models.py --validate. make converge-windmillwas replayed successfully from this worktree branch after the fixes landed, with the final run completing ondocker-runtime,postgres, andproxmox-hostwithout failed hosts.- Production verification confirmed
world_state.current_viewis populated, PostgreSQL holds the graph runtime rows, Windmill schedule arguments are stored inschedule.args, and authenticated gateway traversal returns the expected descendants and shortest path for the current service graph. - A repo-managed graph rebuild also completed successfully from the live guest checkout, proving the deployed worker wrapper can execute the managed scripts directly on
docker-runtime. - Protected integration files still deferred to merge-to-main are
VERSION, release sections inchangelog.md, the top-levelREADME.mdintegrated status summary, andversions/stack.yaml.
- Recursive CTEs for
descendants()must have a cycle guard: include avisitedset in the CTE to avoid infinite loops if a graph cycle is ever introduced by a bad import. Log a warning and return the partial result rather than crashing. - The
health_propagation()method should only propagatedegradedstatus, notdown. Adownservice means the health probe is failing; aderived_health_degradedon a downstream service means "your upstream is sick, you might be at risk". Conflating these two statuses in triage signals would cause confusion. - The
import-from-catalog.pyworkflow readsconfig/workflow-catalog.jsonwhich is on disk in the repo. In Windmill, this means the workflow must clone or read the repo via the git resource. Coordinate with the platform API gateway workstream to ensure the catalog is also accessible via/v1/platform/catalog. - Edge de-duplication: the import workflows run on a schedule and re-insert edges. Use
INSERT INTO graph.edges ... ON CONFLICT DO NOTHINGto avoid duplicate edges accumulating over time. - During ad hoc Windmill API execution, pass
dsnandworld_state_dsnexplicitly to the graph scripts even though the scheduled jobs persist those values inschedule.args; the schedule list endpoint may renderargs: nulleven when the database state is correct.