thermograph/infra/ops/README.md
Emi Griffith 151cf37dfa
All checks were successful
PR build (required check) / changes (pull_request) Successful in 7s
secrets-guard / encrypted (pull_request) Successful in 6s
PR build (required check) / build-frontend (pull_request) Has been skipped
PR build (required check) / validate-observability (pull_request) Has been skipped
PR build (required check) / build-backend (pull_request) Successful in 48s
PR build (required check) / gate (pull_request) Successful in 3s
Add ops/dbq.sh: read-only Postgres queries across all environments
Uniform read-only query access to the LAN dev, beta and prod databases, plus a
thermograph_ro role in each to enforce it.

None of the databases are exposed over TCP: each listens only on its private
docker network, and prod's is a Swarm overlay the host cannot route to -- so
ssh -L works for beta but is impossible for prod, and publishing 5432 would mean
new ufw rules and a Swarm endpoint change on production. Running psql inside the
db container works identically everywhere with no ports, tunnels or infra
changes, so dbq.sh does that: local docker exec for dev, ssh for beta/prod. The
prod container is a Swarm task whose name changes each redeploy, so it is
resolved at call time rather than hardcoded.

Queries connect as thermograph_ro (NOSUPERUSER, granted only pg_read_all_data),
so a stray write fails with "permission denied" even against prod; the app's own
superuser role is deliberately unused here. Extra args pass through to psql and
stdin is forwarded.

ops/README.md documents usage and the conventions Iceberg will follow when it
lands as a sibling (env-keyed dispatch, run the engine where the data is
reachable, dedicated read-only identity).
2026-07-23 14:54:20 -07:00

2.4 KiB

ops/ — operator + agent query tooling

Read-only query access to Thermograph data, uniform across environments.

dbq.sh — Postgres, any environment

infra/ops/dbq.sh dev  "select count(*) from climate_history"
infra/ops/dbq.sh prod -tA -c "select max(date) from climate_history"
infra/ops/dbq.sh beta -c '\dt'
echo "select 1" | infra/ops/dbq.sh prod -f -

Environments: dev (LAN compose stack on the dev machine), beta (75.119.132.91), prod (169.58.46.181). Extra args pass straight through to psql (-tA, -x, --csv, -f, …); stdin is forwarded.

Why it execs into the container instead of using a connection string

None of the databases are exposed over TCP — each listens only on its private docker network. Prod's is a Swarm overlay (10.0.2.0/24) that the host cannot route to, so ssh -L works for beta but is impossible for prod; publishing 5432 would mean new ufw rules plus a Swarm endpoint change on production. Running psql inside the db container works identically in all three environments with no ports, no tunnels, and no infra changes.

The prod container name is a Swarm task name that changes on every redeploy, so it is resolved at call time via docker ps --filter name=…, never hardcoded.

Safety

Queries connect as thermograph_ro — a NOSUPERUSER role granted only pg_read_all_data. Read-only is enforced by Postgres, not by convention:

$ infra/ops/dbq.sh prod -c "create table t(i int)"
ERROR:  permission denied for schema public

The app's own thermograph role is a superuser and is deliberately not used by this tool. If the role is ever missing (fresh database), recreate it with:

CREATE ROLE thermograph_ro LOGIN;
GRANT CONNECT ON DATABASE thermograph TO thermograph_ro;
GRANT pg_read_all_data TO thermograph_ro;

Iceberg (planned)

Iceberg lands as a sibling in this directory following the same three conventions, so the query surface stays uniform:

  1. Env-keyed dispatchiceberg.sh <dev|beta|prod> "<sql>", same argument shape as dbq.sh.
  2. Run the engine where the data is reachable — the catalog/warehouse dictates it (a Trino/DuckDB/pyiceberg invocation, containerised the same way), rather than exposing new network endpoints.
  3. A dedicated read-only identity — the Iceberg equivalent of thermograph_ro, so the same "read-only enforced by the system, not by convention" property holds.