8cf6bedf58
docker compose exec -T подключает stdin и съедал его у скрипта, поданного на stdin (ssh … bash -s): обёртка обрывала такой скрипт после первой команды. Найдено живой проверкой build/cli-live-check.sh (create-user выполнялся, дальше скрипт молча заканчивался). Командам обслуживания stdin не нужен: </dev/null. Плюс e2e web/e2e/query-retry.spec.ts: живая проверка повтора при 429 — запрос участников дважды получает 429 с retry_after_ms и проходит с третьей попытки, раздел «Участники» заполняется без кнопки «Повторить».
308 lines
13 KiB
Bash
Executable File
308 lines
13 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# glchat — служебные команды инстанса (AGENT.md 10.7).
|
|
# Ставится установщиком в /usr/local/bin/glchat.
|
|
set -euo pipefail
|
|
|
|
GLCHAT_ETC_DIR="${GLCHAT_ETC_DIR:-/opt/glchat}"
|
|
COMPOSE_FILE="${GLCHAT_ETC_DIR}/compose.yaml"
|
|
ENV_FILE="${GLCHAT_ETC_DIR}/.env"
|
|
|
|
COMPOSE="docker compose -f ${COMPOSE_FILE}"
|
|
|
|
# app_check_url печатает адрес /readyz с учётом привязки приложения: во внешнем
|
|
# режиме оно слушает WireGuard-адрес, и проверка localhost всегда падала бы.
|
|
app_check_url() {
|
|
local port="$1" host="${2:-}"
|
|
case "$host" in
|
|
0.0.0.0 | "" | "::") host="127.0.0.1" ;;
|
|
esac
|
|
printf 'http://%s:%s/readyz' "$host" "$port"
|
|
}
|
|
|
|
# env_get читает значение из .env инстанса (пусто, если ключа нет).
|
|
env_get() {
|
|
[ -f "$ENV_FILE" ] || return 0
|
|
sed -n "s/^$1=//p" "$ENV_FILE" | tail -1
|
|
}
|
|
|
|
usage() {
|
|
cat <<'EOF'
|
|
glchat — управление инстансом
|
|
|
|
glchat status состояние контейнеров, версия, здоровье, ресурсы
|
|
glchat logs [service] логи (follow)
|
|
glchat restart [service] перезапуск стека или сервиса
|
|
glchat backup бэкап БД и файлов (age-шифрование)
|
|
glchat restore <файл> восстановление из бэкапа
|
|
glchat verify-backup <файл> проверить бэкап, не трогая данные
|
|
glchat update [--channel X] обновление (бэкап → pull → up → smoke)
|
|
glchat reset-password <email> сброс пароля пользователя; --password <пароль>
|
|
glchat make-admin <email> выдать права инстанс-админа
|
|
glchat remove-admin <email> снять права инстанс-админа
|
|
glchat totp-setup <email> включить 2FA и напечатать секрет с резервными кодами
|
|
glchat totp-reset <email> удалить секрет 2FA (потеря устройства и кодов)
|
|
glchat delete-user <email> мягко удалить аккаунт (нужен --yes)
|
|
glchat unban <ip> снять бан fail2ban (jail'ы этого хоста;
|
|
на внешнем прокси — fail2ban-client там)
|
|
glchat fail2ban-status статус jails и забаненных IP
|
|
glchat profile [tier] показать/применить профиль производительности
|
|
glchat security-check аудит: SSH, fail2ban, порты, лимиты
|
|
glchat reindex пересобрать индекс поиска (FTS5)
|
|
glchat vapid-keys сгенерировать пару VAPID-ключей для Web Push
|
|
glchat cleanup чистка: сессии, аудит, файлы без ссылок
|
|
glchat create-user создать аккаунты (--count/--prefix/--sessions)
|
|
glchat change-domain <domain> сменить домен
|
|
glchat uninstall [--keep-data] удалить стек
|
|
glchat version версия CLI и приложения
|
|
EOF
|
|
}
|
|
|
|
require_stack() {
|
|
[ -f "$COMPOSE_FILE" ] || {
|
|
echo "glchat: не найден ${COMPOSE_FILE}; сначала запустите install.sh" >&2
|
|
exit 1
|
|
}
|
|
}
|
|
|
|
cmd_status() {
|
|
require_stack
|
|
local meta version health
|
|
echo "== compose =="
|
|
$COMPOSE ps
|
|
echo
|
|
local check_url
|
|
check_url="$(app_check_url "${APP_PORT:-8080}" "$(env_get GLCHAT_APP_BIND)")"
|
|
meta="$(curl -fsS "${check_url%/readyz}/api/v1/meta" 2>/dev/null || true)"
|
|
version="$(printf '%s' "$meta" | sed -n 's/.*"version":"\([^"]*\)".*/\1/p')"
|
|
health="$(curl -fsS "${check_url}" 2>/dev/null || echo '{"status":"unreachable"}')"
|
|
echo "== app =="
|
|
echo "version: ${version:-unknown}"
|
|
echo "readyz: ${health}"
|
|
echo
|
|
echo "== ресурсы =="
|
|
docker stats --no-stream --format 'table {{.Name}}\t{{.CPUPerc}}\t{{.MemUsage}}' 2>/dev/null || true
|
|
echo
|
|
df -h "${GLCHAT_ETC_DIR}/data" 2>/dev/null | tail -1 || true
|
|
}
|
|
|
|
cmd_logs() {
|
|
require_stack
|
|
$COMPOSE logs -f --tail=200 "$@"
|
|
}
|
|
|
|
cmd_restart() {
|
|
require_stack
|
|
if [ "$#" -gt 0 ]; then
|
|
$COMPOSE restart "$@"
|
|
else
|
|
$COMPOSE restart
|
|
fi
|
|
}
|
|
|
|
cmd_backup() {
|
|
require_stack
|
|
bash "${GLCHAT_ETC_DIR}/backup.sh" "$@"
|
|
}
|
|
|
|
cmd_restore() {
|
|
require_stack
|
|
bash "${GLCHAT_ETC_DIR}/restore.sh" "$@"
|
|
}
|
|
|
|
cmd_update() {
|
|
require_stack
|
|
bash "${GLCHAT_ETC_DIR}/update.sh" "$@"
|
|
}
|
|
|
|
cmd_unban() {
|
|
[ "$#" -ge 1 ] || {
|
|
echo "использование: glchat unban <ip>" >&2
|
|
exit 1
|
|
}
|
|
fail2ban-client unban "$1"
|
|
}
|
|
|
|
cmd_fail2ban_status() {
|
|
fail2ban-client status
|
|
for jail in $(fail2ban-client status | sed -n 's/.*Jail list:\s*//p' | tr ',' ' '); do
|
|
echo
|
|
fail2ban-client status "${jail// /}"
|
|
done
|
|
}
|
|
|
|
# cmd_profile показывает текущий профиль или переключает его. Переключение —
|
|
# это повторная установка, поэтому форма развёртывания (внешний прокси, адреса
|
|
# привязки, внешний TURN) восстанавливается из .env: иначе `glchat profile lite`
|
|
# переписал бы compose на встроенный прокси и сломал инстанс за чужим прокси.
|
|
cmd_profile() {
|
|
if [ "$#" -eq 0 ]; then
|
|
echo "профиль: $(sed -n 's/^PROFILE=//p' "$ENV_FILE" 2>/dev/null || echo unknown)"
|
|
grep -E '^(SQLITE_|GOMAXPROCS|MAX_UPLOAD_SIZE|APP_CPUS|APP_MEMORY_LIMIT)' "$ENV_FILE" 2>/dev/null || true
|
|
return
|
|
fi
|
|
require_stack
|
|
local args=()
|
|
local domain app_bind livekit_bind proxy_mode relay_host
|
|
domain="$(env_get DOMAIN)"
|
|
[ -n "$domain" ] || {
|
|
echo "glchat: в ${ENV_FILE} нет DOMAIN" >&2
|
|
exit 1
|
|
}
|
|
args=(--profile "$1" --reconfigure --yes --domain "$domain")
|
|
proxy_mode="$(env_get PROXY_MODE)"
|
|
if [ "$proxy_mode" = "external" ]; then
|
|
args+=(--external-proxy)
|
|
[ "$(env_get TLS_BY_PROXY)" = "true" ] && args+=(--tls-terminated-by-proxy)
|
|
fi
|
|
app_bind="$(env_get GLCHAT_APP_BIND)"
|
|
[ -n "$app_bind" ] && args+=(--app-bind-addr "$app_bind")
|
|
livekit_bind="$(env_get GLCHAT_LIVEKIT_BIND)"
|
|
[ -n "$livekit_bind" ] && args+=(--livekit-bind-addr "$livekit_bind")
|
|
if [ "$(env_get TURN_ENABLED)" = "false" ]; then
|
|
args+=(--no-turn)
|
|
fi
|
|
relay_host="$(env_get TURN_RELAY_HOST)"
|
|
if [ -n "$relay_host" ]; then
|
|
args+=(--turn-relay-host "$relay_host")
|
|
[ -n "$(env_get TURN_RELAY_PORT)" ] && args+=(--turn-relay-port "$(env_get TURN_RELAY_PORT)")
|
|
[ -n "$(env_get TURN_RELAY_TLS_PORT)" ] && args+=(--turn-relay-tls-port "$(env_get TURN_RELAY_TLS_PORT)")
|
|
[ -n "$(env_get TURN_RELAY_TLS_HOST)" ] && args+=(--turn-relay-tls-host "$(env_get TURN_RELAY_TLS_HOST)")
|
|
fi
|
|
local ip
|
|
for ip in $(env_get FAIL2BAN_IGNOREIP); do
|
|
args+=(--ignore-ip "$ip")
|
|
done
|
|
[ -n "$(env_get ADMIN_EMAIL)" ] && args+=(--admin "$(env_get ADMIN_EMAIL)")
|
|
[ -n "$(env_get ADMIN_PASSWORD)" ] && args+=(--password "$(env_get ADMIN_PASSWORD)")
|
|
echo "==> переключение профиля на $1 (форма развёртывания берётся из .env)"
|
|
GLCHAT_ETC_DIR="$GLCHAT_ETC_DIR" bash "${GLCHAT_ETC_DIR}/install.sh" "${args[@]}"
|
|
}
|
|
|
|
cmd_security_check() {
|
|
bash "${GLCHAT_ETC_DIR}/security-check.sh"
|
|
}
|
|
|
|
cmd_change_domain() {
|
|
[ "$#" -ge 1 ] || {
|
|
echo "использование: glchat change-domain <domain>" >&2
|
|
exit 1
|
|
}
|
|
GLCHAT_ETC_DIR="$GLCHAT_ETC_DIR" bash "${GLCHAT_ETC_DIR}/install.sh" --domain "$1" --reconfigure --yes
|
|
}
|
|
|
|
cmd_uninstall() {
|
|
require_stack
|
|
local keep_data=0
|
|
[ "${1:-}" = "--keep-data" ] && keep_data=1
|
|
echo "Контейнеры glchat будут удалены. Данные: $([ "$keep_data" = 1 ] && echo 'сохраняются' || echo 'УДАЛЯЮТСЯ')"
|
|
read -r -p "Продолжить? (yes/no): " reply
|
|
[ "$reply" = "yes" ] || {
|
|
echo "отменено"
|
|
exit 0
|
|
}
|
|
systemctl disable --now glchat.service 2>/dev/null || true
|
|
$COMPOSE --profile embedded-proxy down --remove-orphans || true
|
|
if [ "$keep_data" = 0 ]; then
|
|
rm -rf "${GLCHAT_ETC_DIR}/data"
|
|
fi
|
|
rm -f /etc/systemd/system/glchat.service
|
|
systemctl daemon-reload
|
|
echo "Стек удалён. Каталог ${GLCHAT_ETC_DIR} оставлен для ручной проверки."
|
|
}
|
|
|
|
cmd_version() {
|
|
echo "glchat CLI 0.1.0 (phase 0)"
|
|
[ -f "$ENV_FILE" ] && grep -E '^(APP_VERSION|IMAGE_TAG|DOMAIN|PROFILE)=' "$ENV_FILE" || true
|
|
}
|
|
|
|
# cmd_verify_backup проверяет архив бэкапа: расшифровка, целостность базы и
|
|
# состав данных (AGENT.md 10.8).
|
|
cmd_verify_backup() {
|
|
require_stack
|
|
[ -n "${1:-}" ] || {
|
|
echo "использование: glchat verify-backup <файл.tar.gz[.age]>" >&2
|
|
exit 2
|
|
}
|
|
GLCHAT_ETC_DIR="${GLCHAT_ETC_DIR}" bash "${GLCHAT_ETC_DIR}/verify-backup.sh" "$@"
|
|
}
|
|
|
|
# app_cli выполняет подкоманду обслуживания внутри контейнера приложения
|
|
# (AGENT.md 10.7): команды, которым нужна БД, живут в Go-бинарнике, а shell-обёртка
|
|
# доставляет их с хоста. `</dev/null` обязателен: `docker compose exec -T`
|
|
# подключает stdin и без этого съедает его у вызывающего скрипта — а обёртку
|
|
# часто зовут из скрипта, поданного на stdin (`ssh … bash -s`), и он обрывался
|
|
# после первой же команды.
|
|
app_cli() {
|
|
require_stack
|
|
${COMPOSE} exec -T app glchat "$@" </dev/null
|
|
}
|
|
|
|
# cmd_reindex пересобирает индекс полнотекстового поиска (AGENT.md 10.7).
|
|
cmd_reindex() {
|
|
app_cli reindex
|
|
}
|
|
|
|
# cmd_vapid_keys печатает новую пару VAPID-ключей (Фаза 7): строки из вывода
|
|
# добавляются в .env инстанса вручную — смена ключа отзывает все подписки,
|
|
# поэтому автоматически её никто не делает.
|
|
cmd_vapid_keys() {
|
|
app_cli vapid-keys "$@"
|
|
}
|
|
|
|
# cmd_cleanup запускает чистку данных вручную: те же правила, что у
|
|
# периодического обслуживания (AGENT.md 6.4).
|
|
cmd_cleanup() {
|
|
app_cli cleanup "$@"
|
|
}
|
|
|
|
# cmd_create_user создаёт аккаунты командой обслуживания: регистрация может
|
|
# быть выключена, а для нагрузочного теста нужны десятки пользователей
|
|
# (AGENT.md 10.7, 11.4). Флаг --sessions сразу выдаёт сессии для генератора.
|
|
cmd_create_user() {
|
|
app_cli create-user "$@"
|
|
}
|
|
|
|
# cmd_account обслуживает аккаунт: сброс пароля, права инстанс-админа, 2FA и
|
|
# мягкое удаление (AGENT.md 10.7) — тонкая обёртка над подкомандами бинарника.
|
|
cmd_account() {
|
|
app_cli "$@"
|
|
}
|
|
|
|
main() {
|
|
local cmd="${1:-help}"
|
|
shift || true
|
|
case "$cmd" in
|
|
status) cmd_status "$@" ;;
|
|
logs) cmd_logs "$@" ;;
|
|
restart) cmd_restart "$@" ;;
|
|
backup) cmd_backup "$@" ;;
|
|
restore) cmd_restore "$@" ;;
|
|
update) cmd_update "$@" ;;
|
|
reset-password | make-admin | remove-admin | totp-setup | totp-reset | delete-user)
|
|
# Команды аккаунтов живут в Go-бинарнике (AGENT.md 10.7): обёртка
|
|
# только доставляет их в контейнер приложения.
|
|
cmd_account "$cmd" "$@"
|
|
;;
|
|
unban) cmd_unban "$@" ;;
|
|
fail2ban-status) cmd_fail2ban_status "$@" ;;
|
|
profile) cmd_profile "$@" ;;
|
|
security-check) cmd_security_check "$@" ;;
|
|
reindex) cmd_reindex "$@" ;;
|
|
vapid-keys) cmd_vapid_keys "$@" ;;
|
|
verify-backup) cmd_verify_backup "$@" ;;
|
|
cleanup) cmd_cleanup "$@" ;;
|
|
create-user) cmd_create_user "$@" ;;
|
|
change-domain) cmd_change_domain "$@" ;;
|
|
uninstall) cmd_uninstall "$@" ;;
|
|
version) cmd_version "$@" ;;
|
|
help | --help | -h) usage ;;
|
|
*)
|
|
echo "glchat: неизвестная команда '$cmd'" >&2
|
|
usage >&2
|
|
exit 1
|
|
;;
|
|
esac
|
|
}
|
|
|
|
main "$@"
|