feat(deploy): установщик, docker-скелет, шаблоны Caddy/LiveKit и CLI
- deploy/install.sh: идемпотентная установка, режимы встроенного и внешнего прокси (§10.13), --dry-run/--skip-tls/--reconfigure, auto-детект профиля (§10.10) - deploy/compose.yaml.tpl: app, livekit, caddy с лимитами, healthcheck, ротацией логов, cap_drop и no-new-privileges (§4.3) - deploy/caddy: Caddyfile.tpl (встроенный) и external-proxy.conf.tpl (наш стенд) - deploy/livekit: конфиг с TURN, dynacast и лимитами комнат (§10.6) - deploy/backup.sh + restore.sh: sqlite .backup, tar, age-шифрование (§10.8) - deploy/update.sh: бэкап → pull/сборка → up → миграции → smoke (§9.3) - deploy/smoke.sh: проверки §10.9 (healthz/readyz/meta, порты, UFW, fail2ban, бэкап) - deploy/security-check.sh: аудит SSH, UFW, fail2ban, прав .env и ключей - deploy/ufw-docker.sh, nftables и fail2ban-конфиги (sshd, glchat-auth, recidive) - deploy/glchat-cli.sh: команды §10.7, systemd-юнит glchat.service - scripts/test-install.sh: прогон установщика в чистом Ubuntu 24.04 (systemd-контейнер) - Dockerfile: multi-stage (node → golang CGO → debian-slim), non-root, healthcheck - Makefile: lint/test/build/dev/generate/security/smoke и фиксация версий инструментов
This commit is contained in:
@@ -0,0 +1,260 @@
|
||||
#!/usr/bin/env bash
|
||||
# Общие функции установщика и служебных скриптов glchat.
|
||||
# Подключается через `source`; сам по себе ничего не выполняет.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
GLCHAT_ETC_DIR="${GLCHAT_ETC_DIR:-/opt/glchat}"
|
||||
GLCHAT_DATA_DIR="${GLCHAT_DATA_DIR:-${GLCHAT_ETC_DIR}/data}"
|
||||
GLCHAT_INSTALL_LOG="${GLCHAT_INSTALL_LOG:-${GLCHAT_ETC_DIR}/install.log}"
|
||||
GLCHAT_ENV_FILE="${GLCHAT_ENV_FILE:-${GLCHAT_ETC_DIR}/.env}"
|
||||
|
||||
GLCHAT_GREEN=$'\033[32m'
|
||||
GLCHAT_YELLOW=$'\033[33m'
|
||||
GLCHAT_RED=$'\033[31m'
|
||||
GLCHAT_BOLD=$'\033[1m'
|
||||
GLCHAT_RESET=$'\033[0m'
|
||||
|
||||
log() {
|
||||
local line
|
||||
line="[$(date -u +%Y-%m-%dT%H:%M:%SZ)] $*"
|
||||
printf '%s\n' "$line"
|
||||
if [ -n "${GLCHAT_INSTALL_LOG:-}" ] && [ -w "$(dirname "$GLCHAT_INSTALL_LOG")" ] 2>/dev/null; then
|
||||
printf '%s\n' "$line" >>"$GLCHAT_INSTALL_LOG"
|
||||
fi
|
||||
}
|
||||
|
||||
log_file_only() {
|
||||
if [ -n "${GLCHAT_INSTALL_LOG:-}" ]; then
|
||||
mkdir -p "$(dirname "$GLCHAT_INSTALL_LOG")" 2>/dev/null || true
|
||||
printf '[%s] %s\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$*" >>"$GLCHAT_INSTALL_LOG" 2>/dev/null || true
|
||||
fi
|
||||
}
|
||||
|
||||
info() { printf '%s==>%s %s\n' "$GLCHAT_BOLD" "$GLCHAT_RESET" "$*"; log "INFO $*"; }
|
||||
ok() { printf '%s ok%s %s\n' "$GLCHAT_GREEN" "$GLCHAT_RESET" "$*"; log "OK $*"; }
|
||||
warn() { printf '%swarn%s %s\n' "$GLCHAT_YELLOW" "$GLCHAT_RESET" "$*" >&2; log "WARN $*"; }
|
||||
die() {
|
||||
printf '%serr %s %s\n' "$GLCHAT_RED" "$GLCHAT_RESET" "$*" >&2
|
||||
log "ERROR $*"
|
||||
exit 1
|
||||
}
|
||||
|
||||
need_root() {
|
||||
if [ "$(id -u)" -ne 0 ]; then
|
||||
die "запустите установщик от root: sudo bash deploy/install.sh"
|
||||
fi
|
||||
}
|
||||
|
||||
have() { command -v "$1" >/dev/null 2>&1; }
|
||||
|
||||
require_cmd() {
|
||||
for cmd in "$@"; do
|
||||
have "$cmd" || die "не найдена обязательная команда: $cmd"
|
||||
done
|
||||
}
|
||||
|
||||
confirm() {
|
||||
local prompt="$1" default="${2:-no}" reply
|
||||
if [ "${GLCHAT_ASSUME_YES:-0}" = "1" ]; then
|
||||
return 0
|
||||
fi
|
||||
if [ ! -t 0 ]; then
|
||||
[ "$default" = "yes" ] && return 0 || return 1
|
||||
fi
|
||||
read -r -p "$prompt " reply || true
|
||||
reply="${reply:-$default}"
|
||||
case "$reply" in
|
||||
y | Y | yes | YES | да | Да) return 0 ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
prompt_value() {
|
||||
local prompt="$1" default="${2:-}" reply
|
||||
if [ ! -t 0 ]; then
|
||||
printf '%s' "$default"
|
||||
return 0
|
||||
fi
|
||||
if [ -n "$default" ]; then
|
||||
read -r -p "$prompt [$default]: " reply || true
|
||||
printf '%s' "${reply:-$default}"
|
||||
else
|
||||
read -r -p "$prompt: " reply || true
|
||||
printf '%s' "$reply"
|
||||
fi
|
||||
}
|
||||
|
||||
prompt_secret() {
|
||||
local prompt="$1" first second
|
||||
if [ ! -t 0 ]; then
|
||||
die "$prompt: требуется интерактивный ввод или флаг --password"
|
||||
fi
|
||||
while true; do
|
||||
read -r -s -p "$prompt: " first || true
|
||||
printf '\n' >&2
|
||||
read -r -s -p "повторите пароль: " second || true
|
||||
printf '\n' >&2
|
||||
if [ "$first" != "$second" ]; then
|
||||
warn "пароли не совпадают, попробуйте снова"
|
||||
continue
|
||||
fi
|
||||
if [ "${#first}" -lt 8 ]; then
|
||||
warn "пароль короче 8 символов"
|
||||
continue
|
||||
fi
|
||||
printf '%s' "$first"
|
||||
return 0
|
||||
done
|
||||
}
|
||||
|
||||
rand_hex() { openssl rand -hex "${1:-32}"; }
|
||||
rand_base64() { openssl rand -base64 "${1:-32}" | tr -d '\n=' | tr '+/' '-_'; }
|
||||
|
||||
json_escape() {
|
||||
local value="$1"
|
||||
value="${value//\\/\\\\}"
|
||||
value="${value//\"/\\\"}"
|
||||
printf '%s' "$value"
|
||||
}
|
||||
|
||||
# --- профили производительности (AGENT.md 10.10) -------------------------
|
||||
|
||||
profile_values() {
|
||||
local tier="$1"
|
||||
case "$tier" in
|
||||
lite)
|
||||
PROFILE_SQLITE_CACHE_MB=8 PROFILE_SQLITE_MMAP_MB=128 PROFILE_SQLITE_READ_POOL=2
|
||||
PROFILE_GOMAXPROCS=1 PROFILE_LRU_ENTRIES=2000
|
||||
PROFILE_LIVEKIT_MAX_PARTICIPANTS=10 PROFILE_LIVEKIT_MAX_VIDEO=4 PROFILE_LIVEKIT_SIMULCAST=0
|
||||
PROFILE_UPLOAD_MB=25 PROFILE_APP_LIMIT=384m PROFILE_LIVEKIT_LIMIT=640m
|
||||
PROFILE_SWAP_GB=2 PROFILE_BACKUP_KEEP=7 PROFILE_SHARE_SOFT=1440p/30 PROFILE_SHARE_MAX=4K/60
|
||||
;;
|
||||
standard)
|
||||
PROFILE_SQLITE_CACHE_MB=16 PROFILE_SQLITE_MMAP_MB=256 PROFILE_SQLITE_READ_POOL=4
|
||||
PROFILE_GOMAXPROCS=2 PROFILE_LRU_ENTRIES=10000
|
||||
PROFILE_LIVEKIT_MAX_PARTICIPANTS=25 PROFILE_LIVEKIT_MAX_VIDEO=8 PROFILE_LIVEKIT_SIMULCAST=2
|
||||
PROFILE_UPLOAD_MB=50 PROFILE_APP_LIMIT=768m PROFILE_LIVEKIT_LIMIT=1536m
|
||||
PROFILE_SWAP_GB=2 PROFILE_BACKUP_KEEP=14 PROFILE_SHARE_SOFT=1440p/30 PROFILE_SHARE_MAX=4K/60
|
||||
;;
|
||||
plus)
|
||||
PROFILE_SQLITE_CACHE_MB=32 PROFILE_SQLITE_MMAP_MB=512 PROFILE_SQLITE_READ_POOL=8
|
||||
PROFILE_GOMAXPROCS=4 PROFILE_LRU_ENTRIES=50000
|
||||
PROFILE_LIVEKIT_MAX_PARTICIPANTS=50 PROFILE_LIVEKIT_MAX_VIDEO=16 PROFILE_LIVEKIT_SIMULCAST=3
|
||||
PROFILE_UPLOAD_MB=100 PROFILE_APP_LIMIT=1536m PROFILE_LIVEKIT_LIMIT=3g
|
||||
PROFILE_SWAP_GB=2 PROFILE_BACKUP_KEEP=30 PROFILE_SHARE_SOFT=4K/30 PROFILE_SHARE_MAX=4K/60
|
||||
;;
|
||||
pro)
|
||||
PROFILE_SQLITE_CACHE_MB=64 PROFILE_SQLITE_MMAP_MB=1024 PROFILE_SQLITE_READ_POOL="$(nproc)"
|
||||
PROFILE_GOMAXPROCS="$(nproc)" PROFILE_LRU_ENTRIES=200000
|
||||
PROFILE_LIVEKIT_MAX_PARTICIPANTS=100 PROFILE_LIVEKIT_MAX_VIDEO=32 PROFILE_LIVEKIT_SIMULCAST=3
|
||||
PROFILE_UPLOAD_MB=250 PROFILE_APP_LIMIT=4g PROFILE_LIVEKIT_LIMIT=6g
|
||||
PROFILE_SWAP_GB=0 PROFILE_BACKUP_KEEP=60 PROFILE_SHARE_SOFT=4K/60 PROFILE_SHARE_MAX=4K/60
|
||||
;;
|
||||
none)
|
||||
PROFILE_SQLITE_CACHE_MB=8 PROFILE_SQLITE_MMAP_MB=128 PROFILE_SQLITE_READ_POOL=2
|
||||
PROFILE_GOMAXPROCS=0 PROFILE_LRU_ENTRIES=2000
|
||||
PROFILE_LIVEKIT_MAX_PARTICIPANTS=10 PROFILE_LIVEKIT_MAX_VIDEO=4 PROFILE_LIVEKIT_SIMULCAST=0
|
||||
PROFILE_UPLOAD_MB=25 PROFILE_APP_LIMIT=512m PROFILE_LIVEKIT_LIMIT=768m
|
||||
PROFILE_SWAP_GB=0 PROFILE_BACKUP_KEEP=7 PROFILE_SHARE_SOFT=1080p/30 PROFILE_SHARE_MAX=4K/60
|
||||
;;
|
||||
*) die "неизвестный профиль: $tier" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
detect_profile() {
|
||||
local cpus ram_mb
|
||||
cpus="$(nproc)"
|
||||
ram_mb="$(awk '/MemTotal/ {printf "%d", $2/1024}' /proc/meminfo)"
|
||||
if [ "$cpus" -le 1 ] && [ "$ram_mb" -lt 3072 ]; then
|
||||
printf 'lite'
|
||||
elif [ "$cpus" -le 2 ] || [ "$ram_mb" -lt 5120 ]; then
|
||||
printf 'standard'
|
||||
elif [ "$cpus" -le 4 ] || [ "$ram_mb" -lt 9216 ]; then
|
||||
printf 'plus'
|
||||
else
|
||||
printf 'pro'
|
||||
fi
|
||||
}
|
||||
|
||||
# --- рендеринг шаблонов ---------------------------------------------------
|
||||
|
||||
render_template() {
|
||||
local src="$1" dst="$2"
|
||||
shift 2
|
||||
local content
|
||||
content="$(cat "$src")"
|
||||
while [ "$#" -gt 0 ]; do
|
||||
local key="${1%%=*}" value="${1#*=}"
|
||||
content="${content//"{{${key}}}"/$value}"
|
||||
done
|
||||
if printf '%s' "$content" | grep -q '{{[A-Z_]\{2,\}}}'; then
|
||||
local leftover
|
||||
leftover="$(printf '%s' "$content" | grep -o '{{[A-Z_]\{2,\}}}' | sort -u | tr '\n' ' ')"
|
||||
warn "в шаблоне $src остались незаполненные плейсхолдеры: $leftover"
|
||||
fi
|
||||
printf '%s\n' "$content" >"$dst"
|
||||
}
|
||||
|
||||
env_get() {
|
||||
local key="$1" file="${2:-$GLCHAT_ENV_FILE}"
|
||||
[ -f "$file" ] || return 1
|
||||
grep -E "^${key}=" "$file" | tail -1 | cut -d= -f2-
|
||||
}
|
||||
|
||||
env_set() {
|
||||
local key="$1" value="$2" file="${3:-$GLCHAT_ENV_FILE}"
|
||||
if grep -qE "^${key}=" "$file" 2>/dev/null; then
|
||||
local tmp
|
||||
tmp="$(mktemp)"
|
||||
awk -v k="$key" -v v="$value" 'BEGIN { FS = "=" } $1 == k { print k "=" v; next } { print }' "$file" >"$tmp"
|
||||
cat "$tmp" >"$file"
|
||||
rm -f "$tmp"
|
||||
else
|
||||
printf '%s=%s\n' "$key" "$value" >>"$file"
|
||||
fi
|
||||
}
|
||||
|
||||
# --- общие проверки -------------------------------------------------------
|
||||
|
||||
check_os() {
|
||||
[ -r /etc/os-release ] || die "не удалось прочитать /etc/os-release"
|
||||
# shellcheck disable=SC1091
|
||||
. /etc/os-release
|
||||
case "${ID:-}" in
|
||||
ubuntu)
|
||||
case "${VERSION_ID:-}" in
|
||||
24.04 | 26.04) ok "ОС: Ubuntu ${VERSION_ID} (${PRETTY_NAME})" ;;
|
||||
*)
|
||||
if [ "${GLCHAT_FORCE_OS:-0}" = "1" ]; then
|
||||
warn "Ubuntu ${VERSION_ID:-?} не в списке проверенных; продолжаем из-за --force-os"
|
||||
else
|
||||
die "поддерживаются Ubuntu 24.04 LTS и 26.04 LTS, обнаружено: ${PRETTY_NAME:-unknown}. Используйте --force-os для продолжения"
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
;;
|
||||
debian)
|
||||
warn "Debian ${VERSION_ID:-?}: установка не проверялась, продолжаем (пакеты Debian-совместимы)"
|
||||
;;
|
||||
*) die "поддерживается Ubuntu 24.04/26.04 LTS, обнаружено: ${PRETTY_NAME:-unknown}" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
check_resources() {
|
||||
local cpus ram_mb disk_mb
|
||||
cpus="$(nproc)"
|
||||
ram_mb="$(awk '/MemTotal/ {printf "%d", $2/1024}' /proc/meminfo)"
|
||||
disk_mb="$(df -Pm / | awk 'NR==2 {print $4}')"
|
||||
info "ресурсы: ${cpus} vCPU, ${ram_mb} MB RAM, ${disk_mb} MB свободно на /"
|
||||
if [ "$ram_mb" -lt 1800 ]; then
|
||||
warn "меньше 2 GB RAM: профиль lite может не уложиться в бюджет (AGENT.md 9.1)"
|
||||
fi
|
||||
if [ "$disk_mb" -lt 8000 ]; then
|
||||
warn "меньше 8 GB свободного места: сборка образа и бэкапы могут не поместиться"
|
||||
fi
|
||||
}
|
||||
|
||||
check_command() {
|
||||
have "$1" && ok "найдено: $1" || warn "отсутствует: $1 (будет установлено)"
|
||||
}
|
||||
Reference in New Issue
Block a user