AkurAI Build
Menu

AkurAI-Build

public

Latest change b2fb6810b8844f39a9752f65118c05ffecf28160 - Fold AkurAI EC2 operations into Build CLI 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