236 lines
8.5 KiB
Markdown
236 lines
8.5 KiB
Markdown
# ahab
|
|
|
|
A wrapper around `docker compose` for our dockerized Django projects, so the
|
|
same commands work in every repository.
|
|
|
|
It reads `docker compose config` to work out which service runs Django and
|
|
which one is the database, so nothing here takes a service name or a `-f`.
|
|
Everything it does, it does by running `docker`, `git` and the Postgres tools.
|
|
|
|
## Installing
|
|
|
|
You will need Rust installed. Clone the repo and run:
|
|
|
|
```bash
|
|
cargo install --path .
|
|
```
|
|
|
|
## Django
|
|
|
|
Each of these runs `docker compose run --rm` against the Django service, so it
|
|
goes through the image's own entrypoint:
|
|
|
|
```bash
|
|
ahab django run <cmd> # anything, in a fresh container
|
|
ahab django bash # a shell in a fresh container
|
|
ahab django manage <args> # manage.py
|
|
ahab django makemigrations
|
|
ahab django migrate <args>
|
|
ahab django shell
|
|
ahab django make-command <app> <name>
|
|
```
|
|
|
|
## Postgres
|
|
|
|
```bash
|
|
ahab postgres dump <path> # pg_dump, custom format
|
|
ahab postgres dump -F plain <path> # pg_dump, plain SQL
|
|
ahab postgres dump -F tar <path> # pg_dump, tar
|
|
ahab postgres dump -F directory <d> # pg_dump, a directory of files
|
|
ahab postgres dump -F cluster <path> # pg_dumpall, roles and all databases
|
|
ahab postgres dump -F plain -z <path> # any of them, gzipped
|
|
ahab postgres import <path> # drop, create, then restore
|
|
ahab postgres psql <args> # psql in the database container
|
|
```
|
|
|
|
Dumps are written `0600`, under a name of their own until they are complete and
|
|
then renamed over the target.
|
|
|
|
Arguments reach `psql` untouched, and Docker is only asked for a terminal when
|
|
there is one to hand over, so both of these work:
|
|
|
|
```bash
|
|
ahab postgres psql # interactive
|
|
ahab postgres psql -tAc 'select count(*) from auth_user' | wc -l
|
|
```
|
|
|
|
### What an import makes of a dump
|
|
|
|
The shape is read from the file rather than from its name:
|
|
|
|
- a `PGDMP` or `toc.dat` header — an archive, restored with `pg_restore`, as is
|
|
a directory produced by `pg_dump -Fd`
|
|
- a `pg_dumpall` header — a whole cluster, which creates its own databases and
|
|
carries role statements, so the database is dropped but not recreated and the
|
|
dump goes to `psql` connected to `postgres`
|
|
- gzip magic — decompressed on the way in, whichever of the three is inside
|
|
- anything else — SQL, fed to `psql` with `ON_ERROR_STOP` and
|
|
`--single-transaction`, so a bad file rolls back instead of half applying
|
|
|
|
A cluster restore runs without `ON_ERROR_STOP`, so the expected complaints
|
|
about roles that already exist do not stop it. Any other error fails the
|
|
import. Importing into a cluster that has never held the database works either
|
|
way.
|
|
|
|
Containers are stopped before an import starts, so one that fails leaves the
|
|
project stopped and says so.
|
|
|
|
## Link
|
|
|
|
A sandbox that mounts the repository can read anything untracked sitting in it
|
|
— a `.env`, a dump, a key. Those paths can be moved out to a store of ahab's
|
|
own and symlinked back: the host resolves the link, the sandbox does not.
|
|
|
|
```bash
|
|
ahab link add .env secrets/ # move out, leave symlinks behind
|
|
ahab link restore .env # move it back into the repository
|
|
ahab link restore --all # everything this repo has linked
|
|
ahab link list # what the store holds for this repository
|
|
ahab link check # what a sandbox can still read
|
|
ahab link check --porcelain # `<code> <path>`, for scripts
|
|
ahab link check --exit-code # exit 4 when anything is outside the store
|
|
```
|
|
|
|
Restoring is the inverse of adding: the file comes back to where it was and the
|
|
store keeps nothing. A second checkout linking the same path is left with a
|
|
dangling symlink, there being only ever one stored copy.
|
|
|
|
Every stored path is in one of three states:
|
|
|
|
```
|
|
$ ahab link list
|
|
store: /home/you/.local/share/ahab/git.aflabs.org/urnik/afurnik
|
|
linked: .env
|
|
shadowed: config.local.py
|
|
missing: secrets/token
|
|
```
|
|
|
|
- `linked` — the symlink is there and points at the store
|
|
- `shadowed` — a real file took the path back, so nothing reads the stored copy
|
|
- `missing` — the symlink is gone, and nothing reads the stored copy either
|
|
|
|
Only a linked path can be moved back, so `restore --all` says how many it left
|
|
alone.
|
|
|
|
Adding refuses two kinds of path:
|
|
|
|
- **a tracked path**, which git would restore anyway. Where one is a symlink
|
|
leading out of the repository, `check` reports it under the code `T>`
|
|
- **a symlink already leading out of the repository**, since moving it would
|
|
move the pointer and leave the contents where they are
|
|
|
|
For scripts there is `--porcelain`, and `-z` for filenames that need it:
|
|
entries end with a NUL, and the two paths of a symlink leading elsewhere are
|
|
separated by one as well, the way `git status -z` reports a rename.
|
|
|
|
The store lives under
|
|
`${XDG_DATA_HOME:-$HOME/.local/share}/ahab/<host>/<owner>/<repo>/`, derived
|
|
from the Git `origin` remote, or `_local/<checkout>` when there is no remote to
|
|
name it after. Its directories are created `0700`. A remote or checkout name
|
|
that needed characters replacing carries a short fingerprint of the original,
|
|
so two of them cannot share a directory.
|
|
|
|
## What ahab makes of your project
|
|
|
|
Running `ahab status` prints the services it picked, the containers behind
|
|
them, the credentials it would use and what the store holds. It reports
|
|
whatever it cannot work out rather than stopping at the first problem:
|
|
|
|
```
|
|
$ ahab status
|
|
services: db, django
|
|
django: django (built from the project)
|
|
container: 0a8b19e27634
|
|
postgres: db (postgres:18-alpine)
|
|
container: 189d4d395d62
|
|
user: myproject
|
|
database: myproject_db
|
|
store: /home/you/.local/share/ahab/git.aflabs.org/urnik/afurnik
|
|
linked: 2
|
|
`ahab link check` lists what a sandbox can still read
|
|
```
|
|
|
|
## Seeing what it runs
|
|
|
|
- `-v` — print each Docker command as it runs
|
|
- `--dry-run` — print the commands it would run, without running them
|
|
- `-q` — print only what was asked for, dropping the progress along the way,
|
|
Compose's own included
|
|
|
|
All three work before or after the subcommand:
|
|
|
|
```bash
|
|
ahab -v django migrate
|
|
ahab --dry-run postgres import ./dump
|
|
ahab -q postgres dump ./dump
|
|
```
|
|
|
|
Nothing is written under `--dry-run`, the filesystem included: `link add` and
|
|
`django make-command` say what they would do and leave the tree alone.
|
|
|
|
## Service detection
|
|
|
|
Services can be named anything; ahab matches on what they are:
|
|
|
|
- **postgres** — the service whose image is a Postgres flavour, matching
|
|
`postg`, `timescale`, `pgvector` or `citus`
|
|
- **django** — the service that both builds an image and sets
|
|
`DJANGO_SETTINGS_MODULE`. Where more than one does, a worker beside the web
|
|
service say, the one publishing ports wins
|
|
|
|
If nothing matches, or two candidates cannot be told apart, ahab says so and
|
|
lists the services it looked at rather than guessing.
|
|
|
|
The role and database come from `POSTGRES_USER` and `POSTGRES_DB` on the
|
|
detected service, falling back to `db` when neither is set. Both have to be
|
|
names: a value holding an `=`, a URL or a leading dash is refused, libpq
|
|
reading such a name as a whole connection string.
|
|
|
|
## The compose file
|
|
|
|
Compose finds its own file and ahab does not pass `-f`, so set Docker's own
|
|
`COMPOSE_FILE` when the file is not in the working directory, including its
|
|
`base.yaml:override.yaml` form. A project's `.env` is a good place for it,
|
|
since Docker reads that too:
|
|
|
|
```
|
|
COMPOSE_FILE=docker/docker-compose.yaml
|
|
```
|
|
|
|
## Shell completion
|
|
|
|
Completion scripts are written to stdout, for Bash, Elvish, Fish, PowerShell
|
|
or Zsh:
|
|
|
|
```bash
|
|
ahab completions zsh > ~/.local/share/zsh/completions/_ahab
|
|
eval "$(ahab completions zsh)" # or one line in .zshrc, never goes stale
|
|
```
|
|
|
|
They are generated during the build as well, landing in
|
|
`target/*/build/*/out/`. To install a single shell's file straight into place:
|
|
|
|
```bash
|
|
SHELL_COMPLETIONS_DIR_ZSH=~/.local/share/zsh/completions \
|
|
SHELL_COMPLETIONS_DIR_FISH=~/.config/fish/completions \
|
|
cargo install --path .
|
|
```
|
|
|
|
Set `SHELL_COMPLETIONS_DIR` to write every shell into one directory instead.
|
|
Either way the path has to be absolute.
|
|
|
|
## Exit codes
|
|
|
|
- `0` — it ran and had nothing to report
|
|
- `1` — it could not finish
|
|
- `2` — the arguments were wrong
|
|
- `4` — it ran fine and found something worth reporting, which today means
|
|
`link check --exit-code` with anything outside the store
|
|
|
|
## Environment variables
|
|
|
|
- `AHAB_LINK_ROOT` — where `ahab link` keeps its store, the same as `--store`,
|
|
which takes precedence. Defaults to
|
|
`${XDG_DATA_HOME:-$HOME/.local/share}/ahab`. Read by `ahab status` too, so it
|
|
reports the store the link commands actually use
|