Initial commit
This commit is contained in:
+256
@@ -0,0 +1,256 @@
|
||||
#!/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}"
|
||||
}
|
||||
Reference in New Issue
Block a user