Skip to content

v0.2-alpha: Docker backend for all 4 plugins (mysql / postgres / redis / elasticsearch)#6

Merged
ikeikeikeike merged 2 commits into
mainfrom
feat/v0.2-docker-backend-mysql
Jun 16, 2026
Merged

v0.2-alpha: Docker backend for all 4 plugins (mysql / postgres / redis / elasticsearch)#6
ikeikeikeike merged 2 commits into
mainfrom
feat/v0.2-docker-backend-mysql

Conversation

@ikeikeikeike

@ikeikeikeike ikeikeikeike commented Jun 16, 2026

Copy link
Copy Markdown
Member

Summary

Λ-5a complete: Docker code path added to all four DB plugins.
Selection via databases[].backend: docker in
.worktree-isolation.yaml; the host injects the choice into
UpReq.Extras["backend"]. Down / ReadyCheck self-detect by looking up
a bough-<engine>-<port> container, so the proto stays unchanged.

The existing nix code path remains the default when backend: is
omitted, so v0.1.x users see no behaviour change.

Why

Λ-2.5b auba dogfood (v0.1.1 ship + flake.lock + 600 s ready timeout)
surfaced two structural bugs in the nix mysql plugin that block real
use:

  1. services-flake's configure-mysql script hits the macOS 104-char
    sun_path limit on deeply-nested worktree datadirs.
  2. The plugin's ReadyCheck shells out to the mysql client without
    a TCP fallback, so it can't see a running mysqld unless the host is
    in a nix shell.

Docker resolves both structurally:

Issue nix services-flake Docker
socket sun_path dataDir-rooted, easy to exceed 104 internal to container, short
client binary host needs it on PATH ships inside image, accessible via docker exec
init user creation services-flake script, fragile one ENV var on the official image

Smoke (darwin/arm64, warm Docker Desktop 29.2.1)

Engine Up ReadyCheck Down Total
mysql 24 s 7.6 s 30 s 64 s
postgres 8.4 s 2.9 s 0.24 s 13.6 s
redis 3.9 s 0.09 s 0.24 s 6.3 s
elasticsearch 19 s 21 s 1.2 s 43.6 s
all 4 ~127 s

vs v0.1.1 nix which never reached ready inside 600 s for mysql.

All four leave no orphan containers behind (label-query verified).

Engine-specific patterns

Each plugin's docker.go carries a verbose comment header pointing at
the testcontainers-go module, the Docker Hub image page, and any
Elastic-specific docs that motivated the choice. The high-level
decisions are:

  • mysql: mysql:8.4, env MYSQL_ALLOW_EMPTY_PASSWORD=yes (dev only),
    wait via TCP + mysqladmin ping
  • postgres: postgres:16-alpine, postgres -c fsync=off for cold-
    start speed, wait via TCP + pg_isready
  • redis: redis:7-alpine, --bind 0.0.0.0 --save 60 1 --appendonly yes, wait via TCP + redis-cli ping
  • elasticsearch:
    docker.elastic.co/elasticsearch/elasticsearch:7.17.29,
    discovery.type=single-node, ES_JAVA_OPTS=-Xms1g -Xmx1g,
    ulimits memlock + nofile, wait via TCP + HTTP GET /

Test plan

  • go build ./... succeeds in nix develop .#ci
  • go test ./... passes (all 12 packages, no skips)
  • mysql smoke (cmd/_smoke-docker-mysql) — clean cycle
  • postgres smoke (cmd/_smoke-docker-postgres) — clean cycle
  • redis smoke (cmd/_smoke-docker-redis) — clean cycle
  • elasticsearch smoke (cmd/_smoke-docker-elasticsearch) — clean
    cycle
  • (Follow-up PR) Λ-5b hybrid auto-detect (nix on PATH + flakes → nix,
    else docker)
  • (Follow-up — Λ-2.5c) auba real-world dogfood with
    backend: docker on all three databases

Out of scope (follow-ups)

  • Hybrid auto mode that detects nix/docker availability at runtime
    (Λ-5b)
  • Refactor the duplicated newDockerClient / pullIfMissing /
    lookupByName / usingDockerBackend helpers into a shared
    pkg/dockerutil/ package (intentionally left duplicated for v0.2-
    alpha so each plugin file is self-contained)
  • proto change for an explicit backend field on UpReq (currently
    passed through Extras map)

The v0.1.x nix backend hit two blockers under dogfood:

  1. macOS 104-char sun_path limit truncates services-flake's
     configure-mysql socket path when worktrees nest deeply, so the
     init script cannot create root@'%' and the host-side ReadyCheck
     probe (which dials 127.0.0.1) is never accepted by mysqld.
  2. The mysql plugin's ReadyCheck has no TCP-fallback; it shells out
     to `mysql -e "SELECT 1"` which requires the mysql client on PATH
     (absent when bough is invoked outside a nix shell).

Both blockers disappear under Docker: the container's internal socket
is short by construction, mysqladmin ships inside the image, and host
clients only ever speak TCP via the published port.

This commit adds the Docker code path to the mysql plugin only — the
other three plugins still use nix exclusively and will follow in
subsequent commits. Selection is via `databases[].backend: docker` in
the YAML, which the host injects into `UpReq.Extras["backend"]`. Down
/ ReadyCheck self-detect by looking up a `bough-mysql-<port>` container
so the plugin stays backward-compatible with the existing proto.

Image: `mysql:8.4` (override with `extras["docker.image"]`).
Wait strategy: TCP dial → mysqladmin ping via `ContainerExecCreate`,
inspired by testcontainers-go modules/mysql.
Stop timeout: 30 s (InnoDB graceful shutdown budget).
Labels: `com.bough.managed=true`, `com.bough.engine=mysql`,
`com.bough.image=mysql:8.4`, `com.bough.host-port=<n>` for
label-query-based orphan cleanup later.

Smoke test (cmd/_smoke-docker-mysql) timing on darwin/arm64,
warm Docker Desktop, empty datadir:

  Up         24.0 s   (image pull mysql:8.4 + create + start)
  ReadyCheck  7.6 s   (TCP probe + mysqladmin ping)
  Down       30.0 s   (full graceful budget)
  Cleanup     0.008 s
  Total      64   s   (vs v0.1.x nix path which never completed)

Docker SDK: `github.com/docker/docker` v28 monorepo path (the
`moby/moby/client` spinoff at v0.4.1 introduces ambiguous-import
errors with the monorepo's vendored copies of the same packages).
Extends the v0.2-alpha Docker backend rollout to the remaining three
plugins so all four engines now ship the dispatch pair (`req.Extras[
"backend"] == "docker"` → docker.go) and back-detection helper that
mysql got in 1d2bd97 / eb41946. Each plugin keeps the existing nix
code path intact for users who set `backend: nix` or omit it.

Per-engine choices (research notes are in the file headers):

  postgres
    - image: postgres:16-alpine (Docker Hub recommended)
    - env: POSTGRES_PASSWORD=bough, POSTGRES_USER=bough,
      POSTGRES_DB=<initial-databases[0]>
    - cmd: `postgres -c fsync=off` — testcontainers' default for dev
      speed; per-worktree env is throwaway
    - wait: TCP + `pg_isready -h localhost` (driver-agnostic so the
      strategy survives image swaps to CockroachDB / YugabyteDB)
    - stop timeout: 15 s

  redis
    - image: redis:7-alpine (~5 MB)
    - cmd: `redis-server --bind 0.0.0.0 --save 60 1 --loglevel warning
      --appendonly yes` (verbatim Docker Hub-recommended persistence
      invocation; bind 0.0.0.0 is intentional — Docker publish target
      is 127.0.0.1:<host-port> so external access stays shut)
    - wait: TCP + `redis-cli ping` → PONG
    - stop timeout: 5 s

  elasticsearch
    - image: docker.elastic.co/elasticsearch/elasticsearch:7.17.29
      (last 7-line LTS with first-class linux/arm64 layer)
    - env: discovery.type=single-node,
      xpack.security.enabled=false,
      cluster.routing.allocation.disk.threshold_enabled=false,
      ES_JAVA_OPTS=-Xms1g -Xmx1g
    - ulimits: memlock=-1:-1, nofile=65535
      (HostConfig.Resources.Ulimits per Elastic 7.17 docs)
    - host: vm.max_map_count >= 262144 is required but not enforced
      by the plugin (Docker Desktop VM and Linux kernel are out of
      bough's control); operators see ES log a warning and fail-fast
      before ReadyCheck times out
    - wait: TCP + HTTP GET / (200 ⇒ yellow-or-better, single-node ES
      is always yellow because no replica to assign)
    - stop timeout: 60 s — translog flush + Lucene commit can run
      long once an index has data

Smoke timings (darwin/arm64, warm Docker Desktop 29.2.1, cold image
pull for postgres / redis / elasticsearch — mysql was already pulled
from the previous run):

  postgres        Up 8.4 s  + Ready 2.9 s  + Down 0.2 s = 13.6 s
  redis           Up 3.9 s  + Ready 0.1 s  + Down 0.2 s =  6.3 s
  elasticsearch   Up 19 s   + Ready 21 s   + Down 1.2 s = 43.6 s

vs the v0.1.x nix path which never reached ready inside 600 s on the
auba dogfood for any of the three.

All three smoke programs leave no orphaned containers behind (label-
query verified after each Cleanup).
@ikeikeikeike ikeikeikeike changed the title v0.2-alpha: Docker backend (mysql plugin, first vertical slice) v0.2-alpha: Docker backend for all 4 plugins (mysql / postgres / redis / elasticsearch) Jun 16, 2026
@ikeikeikeike
ikeikeikeike merged commit c928d4d into main Jun 16, 2026
1 check failed
@ikeikeikeike
ikeikeikeike deleted the feat/v0.2-docker-backend-mysql branch June 16, 2026 16:57
ikeikeikeike added a commit that referenced this pull request Jun 21, 2026
…em0 namespace

Round 4 review #23 surfaced six findings that block v0.6.0 ship.
This commit clears all six in-place; remaining medium / low items
land in follow-up commits.

#1 internal/cli/capability.go: runCompile passed `currentScope(nil,
   ...)`, but currentScope dereferences cfg.Repositories on the
   default path → nil pointer panic on every `bough capability
   compile/preview`. Use an explicit Scope{Level: worktree,
   WorktreeID: "default"} literal until --scope flags land in
   v0.6.x.

#2 + #3 cmd/bough-mcp-server/main.go + internal/mcp/server.go:
   WatchStdin and Server.Run both Read from os.Stdin concurrently,
   so the watchdog stole bytes the JSON-RPC scanner needed. The
   ZOMBIE guard also only triggered on io.EOF, so wrapped errors
   left the goroutine spinning. Both go away by deleting
   WatchStdin entirely and letting Server.Run's bufio.Scanner
   detect EOF — main.go's `defer server.Shutdown()` handles the
   subprocess teardown.

#4 + #5 internal/pluginsign/verifier.go +
   internal/cli/plugin_verify.go: verifyCosign defaulted SigPath
   to ".bundle" but .goreleaser.yaml writes ".sig", and the
   command was missing the --certificate-identity /
   --certificate-oidc-issuer flags cosign 2.x requires for
   keyless verify. Default SigPath now prefers .sig (with .bundle
   fallback for legacy artifacts), CertPath defaults to the
   GoReleaser .pem companion, and the Request grows
   CertIdentity / CertIdentityRegexp / CertOIDCIssuer fields the
   CLI exposes as --cert-identity / --cert-identity-regexp /
   --cert-issuer. PreRunE now validates --pubkey is set for
   minisign and --cert-identity (or regexp) is set for cosign
   BEFORE shelling out so the operator sees an Args error rather
   than a phantom verify failure.

#6 internal/mcp/server.go: handleResourcesRead built
   Scope{Level: level} with no RepoName/WorktreeID, then asked
   the backend for a count. sqlite's scopeID returns "/" for that
   shape, so the filter never matched a real worktree row and
   the resource reported `worktree: 0` even when worktree rows
   existed. The honest fix is to stop returning a faithful count
   the host cannot guarantee: readScopesResource now lists the
   three scope tiers with descriptions and points clients at
   memory.query for counts (v0.6.x will add an explicit count API
   once Server learns about cfg.Repositories).

#7 (review #23) plugins/memory/mem0/namespace.go: scopeToMem0
   worktree case derived rootHash from `RepoName + "/" +
   WorktreeID`, so two worktrees of the same repo got different
   user_ids and the documented user-tier sharing did not hold.
   Use hashShort(RepoName) for both segments; branch identity
   continues to live entirely in session_id.

Build clean, all unit + conformance tests pass, lint 0 issues.
ikeikeikeike added a commit that referenced this pull request Jun 21, 2026
…em0 namespace

Round 4 review #23 surfaced six findings that block v0.6.0 ship.
This commit clears all six in-place; remaining medium / low items
land in follow-up commits.

#1 internal/cli/capability.go: runCompile passed `currentScope(nil,
   ...)`, but currentScope dereferences cfg.Repositories on the
   default path → nil pointer panic on every `bough capability
   compile/preview`. Use an explicit Scope{Level: worktree,
   WorktreeID: "default"} literal until --scope flags land in
   v0.6.x.

#2 + #3 cmd/bough-mcp-server/main.go + internal/mcp/server.go:
   WatchStdin and Server.Run both Read from os.Stdin concurrently,
   so the watchdog stole bytes the JSON-RPC scanner needed. The
   ZOMBIE guard also only triggered on io.EOF, so wrapped errors
   left the goroutine spinning. Both go away by deleting
   WatchStdin entirely and letting Server.Run's bufio.Scanner
   detect EOF — main.go's `defer server.Shutdown()` handles the
   subprocess teardown.

#4 + #5 internal/pluginsign/verifier.go +
   internal/cli/plugin_verify.go: verifyCosign defaulted SigPath
   to ".bundle" but .goreleaser.yaml writes ".sig", and the
   command was missing the --certificate-identity /
   --certificate-oidc-issuer flags cosign 2.x requires for
   keyless verify. Default SigPath now prefers .sig (with .bundle
   fallback for legacy artifacts), CertPath defaults to the
   GoReleaser .pem companion, and the Request grows
   CertIdentity / CertIdentityRegexp / CertOIDCIssuer fields the
   CLI exposes as --cert-identity / --cert-identity-regexp /
   --cert-issuer. PreRunE now validates --pubkey is set for
   minisign and --cert-identity (or regexp) is set for cosign
   BEFORE shelling out so the operator sees an Args error rather
   than a phantom verify failure.

#6 internal/mcp/server.go: handleResourcesRead built
   Scope{Level: level} with no RepoName/WorktreeID, then asked
   the backend for a count. sqlite's scopeID returns "/" for that
   shape, so the filter never matched a real worktree row and
   the resource reported `worktree: 0` even when worktree rows
   existed. The honest fix is to stop returning a faithful count
   the host cannot guarantee: readScopesResource now lists the
   three scope tiers with descriptions and points clients at
   memory.query for counts (v0.6.x will add an explicit count API
   once Server learns about cfg.Repositories).

#7 (review #23) plugins/memory/mem0/namespace.go: scopeToMem0
   worktree case derived rootHash from `RepoName + "/" +
   WorktreeID`, so two worktrees of the same repo got different
   user_ids and the documented user-tier sharing did not hold.
   Use hashShort(RepoName) for both segments; branch identity
   continues to live entirely in session_id.

Build clean, all unit + conformance tests pass, lint 0 issues.
ikeikeikeike added a commit that referenced this pull request Jul 3, 2026
fix: retrospective review fixes for merged PRs #1/#2/#3/#4/#5/#6/#7/#8/#14/#16/#17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant