Files
2026-07-14 12:10:22 +03:00

257 lines
7.6 KiB
Bash

#!/usr/bin/env bash
# Shared helpers for FromChat deployment installer.
set -euo pipefail
NC=$'\033[0m'
GRAY=$'\033[38;5;245m'
BLUE=$'\033[38;5;81m'
PURPLE=$'\033[38;5;141m'
RED=$'\033[38;5;203m'
LIME=$'\033[38;5;154m'
ORANGE=$'\033[38;5;208m'
YELLOW=$'\033[38;5;226m'
CHECK=$'\033[38;5;154m'
CROSS=$'\033[38;5;203m'
info() { printf '%b%s%b\n' "$BLUE" "$*" "$NC"; }
success() { printf '%b✓ %s%b\n' "$LIME" "$*" "$NC"; }
warn() { printf '%b⚠ %s%b\n' "$YELLOW" "$*" "$NC"; }
error() { printf '%b✗ %s%b\n' "$RED" "$*" "$NC" >&2; }
step() { printf '\n%b▸ %s%b\n' "$PURPLE" "$*" "$NC"; }
prompt() { printf '%b%s%b ' "$ORANGE" "$*" "$NC" >&2; }
die() {
error "$1"
exit "${2:-1}"
}
require_root() {
if [[ "${EUID}" -ne 0 ]]; then
info "Elevating privileges with sudo…"
exec sudo -E bash "$0" "$@"
fi
}
is_debian_based() {
[[ -f /etc/debian_version ]] || grep -qiE 'ubuntu|debian|mint|pop|elementary|raspbian' /etc/os-release 2>/dev/null
}
require_debian() {
if ! is_debian_based; then
die "This installer supports Debian-based Linux only (Debian, Ubuntu, etc.)."
fi
success "Debian-based OS detected"
}
docker_installed() {
command -v docker >/dev/null 2>&1 && docker info >/dev/null 2>&1
}
install_docker_if_needed() {
if docker_installed; then
success "Docker is already installed"
return 0
fi
warn "Docker is not installed or not running."
prompt "Install Docker using the official script? [Y/n]:"
local answer
IFS= read -r answer || true
answer="${answer:-Y}"
if [[ ! "${answer}" =~ ^[Yy] ]]; then
die "Docker is required. Install it manually: https://docs.docker.com/engine/install/"
fi
step "Installing Docker…"
curl -fsSL https://get.docker.com | sh
systemctl enable --now docker 2>/dev/null || true
success "Docker installed"
}
ensure_python_yaml() {
python3 - <<'PY' >/dev/null 2>&1 && return 0
import yaml # noqa: F401
PY
step "Installing python3-yaml…"
apt-get update -qq
DEBIAN_FRONTEND=noninteractive apt-get install -y -qq python3-yaml
}
ensure_compose_plugin() {
if docker compose version >/dev/null 2>&1; then
success "Docker Compose plugin available"
return 0
fi
step "Installing docker-compose-plugin…"
apt-get update -qq
DEBIAN_FRONTEND=noninteractive apt-get install -y -qq docker-compose-plugin
}
repo_slug_from_url() {
local url="$1"
url="${url%.git}"
for prefix in "github.com/" "git.fromchat.ru/" "https://" "http://"; do
url="${url#*${prefix}}"
done
echo "${url%%/*}/${url##*/}"
}
fetch_raw_file() {
local repo_url="$1"
local tag="$2"
local path="$3"
local dest="$4"
local script="${DEPLOYMENT_ROOT}/scripts/git_remote.py"
local token="${GIT_TOKEN:-${GITHUB_TOKEN:-}}"
if [[ -n "${token}" ]]; then
python3 "${script}" fetch-raw "${repo_url}" "${tag}" "${path}" "${token}" > "${dest}"
else
python3 "${script}" fetch-raw "${repo_url}" "${tag}" "${path}" > "${dest}"
fi
}
get_latest_semver_tag() {
local repo_url="$1"
local token="${GIT_TOKEN:-${GITHUB_TOKEN:-}}"
if [[ -n "${token}" ]]; then
python3 "${DEPLOYMENT_ROOT}/scripts/resolve-tag.py" "${token}" "${repo_url}"
else
python3 "${DEPLOYMENT_ROOT}/scripts/resolve-tag.py" "${repo_url}"
fi
}
select_components() {
if command -v whiptail >/dev/null 2>&1; then
local result
result="$(whiptail --title "FromChat components" --checklist \
"Select components (Space toggles, Enter confirms)" 18 72 4 \
backend "Backend (API, DB, LiveKit)" ON \
frontend "Web frontend" ON \
caddy "Caddy reverse proxy (TLS)" OFF \
updater "Auto-update service" OFF \
3>&1 1>&2 2>&3)" || die "Component selection cancelled."
SELECTED=()
local item
for item in ${result}; do
SELECTED+=("${item//\"/}")
done
((${#SELECTED[@]} > 0)) || die "Select at least one component."
return 0
fi
warn "whiptail not found; using text menu."
local -a options=(backend frontend caddy updater)
local -a labels=(
"Backend (API, DB, LiveKit)"
"Web frontend"
"Caddy reverse proxy"
"Auto-update service"
)
SELECTED=()
local i choice
for i in "${!options[@]}"; do
prompt "Include ${labels[$i]}? [y/N]:"
IFS= read -r choice || true
if [[ "${choice:-}" =~ ^[Yy] ]]; then
SELECTED+=("${options[$i]}")
fi
done
((${#SELECTED[@]} > 0)) || die "Select at least one component."
}
component_selected() {
local needle="$1"
local item
for item in "${SELECTED[@]:-}"; do
[[ "${item}" == "${needle}" ]] && return 0
done
return 1
}
wait_for_compose_healthy() {
local dir="$1"
local timeout="${2:-600}"
step "Waiting for services to become healthy (timeout ${timeout}s)…"
(
cd "${dir}"
docker compose up -d --wait --timeout "${timeout}"
)
}
print_success_banner() {
local dir="$1"
printf '\n%b╔══════════════════════════════════════════════════╗%b\n' "$LIME" "$NC"
printf '%b║ FromChat server installed successfully! ║%b\n' "$LIME" "$NC"
printf '%b╚══════════════════════════════════════════════════╝%b\n\n' "$LIME" "$NC"
info "Install directory: ${dir}"
info "Version tag: ${FROMCHAT_VERSION}"
info "Manage stack: cd ${dir} && docker compose ps"
info "View logs: cd ${dir} && docker compose logs -f"
if component_selected caddy; then
info "Caddy config: ${dir}/Caddyfile"
fi
}
# Merge component compose files into ${install_dir}/compose.yml (image: fromchat/*:<tag>).
# Optional local compose paths (skip remote fetch when set):
# LOCAL_BACKEND_COMPOSE, LOCAL_FRONTEND_COMPOSE
# Requires: DEPLOYMENT_ROOT, BACKEND_REPO, WEB_REPO (for remote fetch).
run_generate_compose() {
local install_dir="$1"
local components_csv="$2"
local tag="$3"
local tmp
tmp="$(mktemp -d)"
step "Generating compose.yml for tag ${tag} (components: ${components_csv})…"
local need_backend=false need_frontend=false
[[ ",${components_csv}," == *,backend,* ]] && need_backend=true
[[ ",${components_csv}," == *,frontend,* ]] && need_frontend=true
if ${need_backend}; then
if [[ -n "${LOCAL_BACKEND_COMPOSE:-}" && -f "${LOCAL_BACKEND_COMPOSE}" ]]; then
cp -f "${LOCAL_BACKEND_COMPOSE}" "${tmp}/backend.compose.yml"
success "Backend compose.yml (local)"
else
fetch_raw_file "${BACKEND_REPO}" "${tag}" "compose.yml" \
"${tmp}/backend.compose.yml"
success "Backend compose.yml"
fi
fi
if ${need_frontend}; then
if [[ -n "${LOCAL_FRONTEND_COMPOSE:-}" && -f "${LOCAL_FRONTEND_COMPOSE}" ]]; then
cp -f "${LOCAL_FRONTEND_COMPOSE}" "${tmp}/frontend.compose.yml"
success "Frontend compose.yml (local)"
else
fetch_raw_file "${WEB_REPO}" "${tag}" "compose.yml" \
"${tmp}/frontend.compose.yml"
success "Frontend compose.yml"
fi
fi
mkdir -p "${install_dir}/config"
cp -f "${DEPLOYMENT_ROOT}/config/livekit.yaml" "${install_dir}/config/livekit.yaml"
local -a gen_args=(
python3 "${DEPLOYMENT_ROOT}/scripts/generate-compose.py"
--tag "${tag}"
--components "${components_csv}"
--output "${install_dir}/compose.yml"
)
if ${need_backend}; then
gen_args+=(--backend-compose "${tmp}/backend.compose.yml")
fi
if ${need_frontend}; then
gen_args+=(--frontend-compose "${tmp}/frontend.compose.yml")
fi
if [[ ",${components_csv}," == *,caddy,* ]]; then
gen_args+=(--caddy-compose "${DEPLOYMENT_ROOT}/compose/caddy.compose.yml")
fi
"${gen_args[@]}"
echo "${tag}" > "${install_dir}/.fromchat-version"
rm -rf "${tmp}"
}