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

61 lines
2.4 KiB
Markdown

# ops/ — operator + agent query tooling
Read-only query access to Thermograph data, uniform across environments.
## `dbq.sh` — Postgres, any environment
```sh
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:
```sql
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 dispatch**`iceberg.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.