Menu
AkurAI-Build
publicLatest change da0263fcb23dbed1e9f59104c3f56b4cfbd568cc - Update EC2 operation documentation and helpers by Ólafur Búi Ólafsson
#!/bin/sh
# akurai-mariadbctl — operate the local /opt/akurai-mariadb docker compose
# MariaDB stack (mariadb-prod :3306, mariadb-dev :3307). A dev-only mode keeps
# the production volume/backup but removes its container until a production VM
# exists. Same layout on Titan and the AkurAI EC2 host; on EC2 drive it remotely
# via `akurai ec2 mariadb <command...>`.
#
# Secret handling: passwords never travel via argv. create-user reads the
# password from stdin (pipe it from a 0400 secret file or PassVault-derived
# material); root credentials stay inside the containers' /run/secrets files.
set -eu
ROOT=/opt/akurai-mariadb
DEV_ONLY_MARKER="$ROOT/.dev-only"
OVERRIDE="$ROOT/docker-compose.override.yml"
MANAGED_OVERRIDE='# managed by akurai-mariadbctl: dev-only'
SUDO=""
FS_SUDO=""
if [ "$(id -u)" -ne 0 ]; then
FS_SUDO="sudo"
docker info >/dev/null 2>&1 || SUDO="sudo"
fi
usage() {
cat <<'EOF'
usage: akurai-mariadbctl <command> [args]
status compose state, memory, databases per instance
health healthcheck verdict (exit 1 unless both healthy)
up | down | restart compose lifecycle for the whole stack
mode [dual|dev-only] show/set active instances; dev-only removes prod
root-shell [prod|dev] interactive root SQL shell
sql <prod|dev> [database] execute SQL from stdin as root
create-db <name> [prod|dev] CREATE DATABASE IF NOT EXISTS (utf8mb4)
create-user <user> <db> [prod|dev]
create user + scoped grant; password on stdin
dump <database> [prod|dev] mariadb-dump to stdout (pipe to a file)
copy-db <db> <from> <to> replace <db> on to with verified copy from from
logs [prod|dev] [lines] container logs (default 50 lines)
EOF
exit "${1:-0}"
}
container() {
case "${1:-prod}" in
prod) echo mariadb-prod ;;
dev) echo mariadb-dev ;;
*) echo "unknown instance: $1 (use prod|dev)" >&2; exit 2 ;;
esac
}
healthy() {
[ "$($SUDO docker inspect -f '{{.State.Health.Status}}' "$1" 2>/dev/null || echo missing)" = healthy ]
}
dump_db() { # $1 container; $2 database; SQL on stdout
$SUDO docker exec -i "$1" sh -c \
'exec mariadb-dump -uroot -p"$(cat /run/secrets/root.pass)" --single-transaction --skip-comments --skip-dump-date --hex-blob --routines --events --triggers "$1"' \
sh "$2"
}
root_sql() { # $1 container; SQL on stdin
$SUDO docker exec -i "$1" sh -c 'exec mariadb -uroot -p"$(cat /run/secrets/root.pass)" --batch'
}
cmd="${1:-}"; [ $# -gt 0 ] && shift
case "$cmd" in
status)
(cd "$ROOT" && $SUDO docker compose ps)
containers="mariadb-dev"
[ -f "$DEV_ONLY_MARKER" ] || containers="mariadb-prod $containers"
$SUDO docker stats --no-stream --format '{{.Name}} mem={{.MemUsage}} cpu={{.CPUPerc}}' $containers 2>/dev/null || true
[ -f "$DEV_ONLY_MARKER" ] && echo "mode: dev-only (production container retired; volume preserved)" || echo "mode: dual"
for instance in prod dev; do
if [ "$instance" = prod ] && [ -f "$DEV_ONLY_MARKER" ]; then
echo "prod databases: disabled"
continue
fi
c=$(container "$instance")
printf '%s databases: ' "$instance"
echo 'SHOW DATABASES' | root_sql "$c" 2>/dev/null | grep -vE '^(Database|information_schema|performance_schema|mysql|sys)$' | tr '\n' ' '
echo
done
;;
health)
ok=0
containers="mariadb-prod mariadb-dev"
[ -f "$DEV_ONLY_MARKER" ] && containers="mariadb-dev"
for c in $containers; do
state=$($SUDO docker inspect -f '{{.State.Health.Status}}' "$c" 2>/dev/null || echo missing)
echo "$c: $state"
[ "$state" = healthy ] || ok=1
done
exit $ok
;;
up) (cd "$ROOT" && $SUDO docker compose up -d) ;;
down) (cd "$ROOT" && $SUDO docker compose down) ;;
restart) (cd "$ROOT" && $SUDO docker compose restart) ;;
mode)
case "${1:-}" in
'')
[ -f "$DEV_ONLY_MARKER" ] && echo dev-only || echo dual
;;
dev-only)
healthy mariadb-dev || { echo "mariadb-dev is not healthy" >&2; exit 1; }
if [ -e "$OVERRIDE" ] && ! grep -qxF "$MANAGED_OVERRIDE" "$OVERRIDE"; then
echo "$OVERRIDE exists and is not managed by akurai-mariadbctl" >&2
exit 1
fi
tmp=$(mktemp)
trap 'rm -f "$tmp"' EXIT
printf '%s\nservices:\n mariadb-prod:\n profiles: ["prod"]\n' "$MANAGED_OVERRIDE" > "$tmp"
$FS_SUDO install -o root -g root -m 644 "$tmp" "$OVERRIDE"
(cd "$ROOT" && $SUDO docker compose stop mariadb-prod && $SUDO docker compose rm -f mariadb-prod)
$FS_SUDO touch "$DEV_ONLY_MARKER"
echo "mode: dev-only (mariadb-prod container removed; volume preserved)"
;;
dual)
if [ -e "$OVERRIDE" ]; then
grep -qxF "$MANAGED_OVERRIDE" "$OVERRIDE" || {
echo "$OVERRIDE is not managed by akurai-mariadbctl" >&2
exit 1
}
$FS_SUDO rm -f "$OVERRIDE"
fi
$FS_SUDO rm -f "$DEV_ONLY_MARKER"
(cd "$ROOT" && $SUDO docker compose up -d)
echo "mode: dual"
;;
*) echo "unknown mode: $1 (use dual|dev-only)" >&2; exit 2 ;;
esac
;;
root-shell)
c=$(container "${1:-prod}")
exec $SUDO docker exec -it "$c" sh -c 'exec mariadb -uroot -p"$(cat /run/secrets/root.pass)"'
;;
sql)
[ $# -ge 1 ] || usage 2
c=$(container "$1")
if [ $# -ge 2 ]; then
$SUDO docker exec -i "$c" sh -c 'exec mariadb -uroot -p"$(cat /run/secrets/root.pass)" --batch "$1"' sh "$2"
else
root_sql "$c"
fi
;;
create-db)
[ $# -ge 1 ] || usage 2
c=$(container "${2:-prod}")
printf 'CREATE DATABASE IF NOT EXISTS `%s` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;\n' "$1" | root_sql "$c"
echo "created: $1 on ${2:-prod}"
;;
create-user)
[ $# -ge 2 ] || usage 2
user="$1"; db="$2"; instance="${3:-prod}"; c=$(container "$instance")
case "$user:$db" in *[!A-Za-z0-9_:]*|:*) echo "invalid user or database name" >&2; exit 2 ;; esac
password=$(cat)
[ -n "$password" ] || { echo "empty password on stdin" >&2; exit 2; }
escaped=$(printf '%s' "$password" | sed -e 's/\\/\\\\/g' -e "s/'/''/g")
printf "CREATE USER IF NOT EXISTS '%s'@'%%' IDENTIFIED BY '%s';\nALTER USER '%s'@'%%' IDENTIFIED BY '%s';\nGRANT ALL PRIVILEGES ON \`%s\`.* TO '%s'@'%%';\nFLUSH PRIVILEGES;\n" \
"$user" "$escaped" "$user" "$escaped" "$db" "$user" | root_sql "$c"
umask 077
secret_tmp=$(mktemp)
trap 'rm -f "$secret_tmp"' EXIT
printf '%s' "$password" > "$secret_tmp"
$FS_SUDO install -o 999 -g 999 -m 400 "$secret_tmp" "$ROOT/secrets/$instance-$user.pass"
unset password escaped
echo "created: $user -> $db on $instance"
;;
copy-db)
[ $# -eq 3 ] || usage 2
db="$1"; from="$2"; to="$3"
case "$db" in *[!A-Za-z0-9_]*|'') echo "invalid database name" >&2; exit 2 ;; esac
[ "$from" != "$to" ] || { echo "source and destination must differ" >&2; exit 2; }
source_container=$(container "$from")
destination_container=$(container "$to")
healthy "$source_container" || { echo "$source_container is not healthy" >&2; exit 1; }
healthy "$destination_container" || { echo "$destination_container is not healthy" >&2; exit 1; }
umask 077
source_dump=$(mktemp)
destination_dump=$(mktemp)
trap 'rm -f "$source_dump" "$destination_dump"' EXIT
dump_db "$source_container" "$db" > "$source_dump"
printf 'DROP DATABASE IF EXISTS `%s`;\nCREATE DATABASE `%s` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;\n' "$db" "$db" |
root_sql "$destination_container"
$SUDO docker exec -i "$destination_container" sh -c \
'exec mariadb -uroot -p"$(cat /run/secrets/root.pass)" --batch "$1"' sh "$db" < "$source_dump"
dump_db "$destination_container" "$db" > "$destination_dump"
cmp -s "$source_dump" "$destination_dump" || {
echo "copy verification failed" >&2
exit 1
}
backup_dir="$ROOT/backups"
backup_name="$db-$from-before-copy-$(date -u +%Y%m%dT%H%M%SZ).sql"
$FS_SUDO install -d -o root -g root -m 700 "$backup_dir"
$FS_SUDO install -o root -g root -m 600 "$source_dump" "$backup_dir/$backup_name"
echo "copied and verified: $db $from -> $to; source backup retained"
;;
dump)
[ $# -ge 1 ] || usage 2
c=$(container "${2:-prod}")
dump_db "$c" "$1"
;;
logs)
c=$(container "${1:-prod}")
$SUDO docker logs --tail "${2:-50}" "$c"
;;
''|-h|--help|help) usage ;;
*) echo "unknown command: $cmd" >&2; usage 2 ;;
esac