Why DevOpsPad is read-only first

DevOpsPad’s query workspace reads and never writes. You can browse MySQL, ClickHouse, MongoDB, Redis, Kafka and Kubernetes, run queries and inspect results, but there is no edit grid, no “save changes” and no DELETE button. This post explains why the line is drawn there, how each adapter holds it, and where the app does write: a separate, deliberately narrow path in the Infra console.

The problem

A desktop client holds working credentials for production systems. The usual way a GUI client causes an incident is not a bug in the client. It is a person in the wrong tab: an UPDATE run against production instead of staging, a key deleted from the wrong Redis, a Deployment edited in the wrong context. The cost of a read query in the wrong place is a wasted minute. The cost of a write in the wrong place can be a restore from backup.

Most of what people do in these tools during an incident is reading anyway: what is in this table, why is this Pod restarting, which partition lost its leader, what does this ConfigMap contain. So we built the reading half first and made it hard to turn into a writing tool by accident.

The design: every adapter only reads

Each engine has its own enforcement, because “read-only” means something different in each:

Engine How reads are enforced in the app
MySQL Each query runs in a server-side read-only transaction (START TRANSACTION READ ONLY), then rolls back
ClickHouse Every HTTP request carries readonly=1, plus max_result_rows=500, result_overflow_mode=break and max_execution_time=15
Redis An allowlist of 20 commands: GET, MGET, GETRANGE, LRANGE, XRANGE, TYPE, TTL, PTTL, SCAN, HSCAN, SSCAN, ZSCAN, HGET, HLEN, LLEN, SCARD, ZCARD, DBSIZE, INFO, PING. Anything else is refused before it is sent
MongoDB The editor accepts a find request only: collection, filter, projection, sort, skip, limit
Kafka Cluster metadata only: topics, partitions, leaders, replica and ISR counts
Kubernetes / K3s get <resource> [namespace] for pods, deployments, services, namespaces, nodes, configmaps and events, plus details, events and log snapshots

On top of that, every result is bounded: 500 rows or documents, an 8 MiB encoded budget where the engine can enforce it, and a 20-second outer timeout. A read can still be expensive, and the bounds stop one careless SELECT * from freezing the app.

Two other rules follow the same idea. AI suggestions are inserted into the editor and never run on their own. Restored query drafts come back as text, and no query is executed at startup.

What the app’s read-only mode does not guarantee

App-level enforcement is defense in depth, not a permission system, and the MySQL row of that table shows why. A read-only transaction rejects INSERT, UPDATE and DELETE. But in MySQL most DDL statements, such as DROP, ALTER, CREATE and TRUNCATE, cause an implicit commit, which ends the read-only transaction and then executes outside it. So the transaction flag does not reliably block DDL. The same is true one way or another for every client-side check: it protects against slips in this app, not against the credentials being used elsewhere.

That is why the docs say, every time: connect with an account that can only read. A SELECT-only MySQL user, a ClickHouse user with only SELECT grants, a Redis ACL user without write commands and a Kubernetes Role without write verbs are the real guarantee. The client’s checks make it hard to do the wrong thing in the client. The read-only MySQL user guide walks through the account side.

Where DevOpsPad does write

Small teams also need to ship, so the Infra console has explicit write paths, kept apart from the query adapters:

  • Deployment actions: release an image by digest, scale, restart, and roll back to a previous release. These live in internal/release, not in the query code, and each one goes through a plan: server-side dry-run, review, a cluster Lease, resource-version checks, rollout verification and a written record. The plans and leases post covers that design.
  • ConfigMap edits: saved to the cluster only when you explicitly choose to, with UID and resourceVersion checks, so a newer version in the cluster is never overwritten silently.
  • Host Nginx configuration: limited to .conf files in /etc/nginx/conf.d/ over a saved SSH connection, validated with nginx -t before systemctl reload nginx, with the original file restored on failure.

None of these are reachable from the query editor. Typing in a query tab cannot reach them.

Trade-offs

  • You cannot fix data in DevOpsPad. Correcting a row or deleting a stuck key needs another tool or a migration. That is intentional for now, and it does cost a context switch.
  • Allowlists lag behind engines. A useful read-only Redis command that is not on the list is refused until we add it.
  • Cancelling stops waiting, not always the server. The client abandons the request, but a database may finish the query until its own timeout.
  • Bounded previews are previews. For a full export, use the engine’s own tooling.

What’s next

The roadmap in the repository keeps the database workspace read-only and treats data editing as a “later, maybe” item. The planned work goes the other way: the three write paths above move into a single action engine (plan → approve → apply → verify → record), and policy and approval come before AI tools get any write capability. Those are plans, not promises. The current boundaries are documented in read-only by design and result limits, and on the security page.