378 lines
14 KiB
Bash
378 lines
14 KiB
Bash
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
CAMELEER_INSTALLER_VERSION="1.0.0"
|
|
CAMELEER_DEFAULT_VERSION="latest"
|
|
REGISTRY="gitea.siegeln.net/cameleer"
|
|
|
|
# --- Colors ---
|
|
RED='\033[0;31m'
|
|
GREEN='\033[0;32m'
|
|
YELLOW='\033[1;33m'
|
|
BLUE='\033[0;34m'
|
|
BOLD='\033[1m'
|
|
NC='\033[0m'
|
|
|
|
# --- Defaults ---
|
|
DEFAULT_INSTALL_DIR="./cameleer"
|
|
DEFAULT_PUBLIC_PROTOCOL="https"
|
|
DEFAULT_ADMIN_USER="admin"
|
|
DEFAULT_TLS_MODE="self-signed"
|
|
DEFAULT_HTTP_PORT="80"
|
|
DEFAULT_HTTPS_PORT="443"
|
|
DEFAULT_LOGTO_CONSOLE_PORT="3002"
|
|
DEFAULT_LOGTO_CONSOLE_EXPOSED="true"
|
|
DEFAULT_VENDOR_ENABLED="false"
|
|
DEFAULT_VENDOR_USER="vendor"
|
|
DEFAULT_COMPOSE_PROJECT="cameleer-saas"
|
|
DEFAULT_DOCKER_SOCKET="/var/run/docker.sock"
|
|
|
|
# --- Config values (set by args/env/config/prompts) ---
|
|
# Save environment values before initialization (CLI args override these)
|
|
_ENV_PUBLIC_HOST="${PUBLIC_HOST:-}"
|
|
_ENV_PUBLIC_PROTOCOL="${PUBLIC_PROTOCOL:-}"
|
|
_ENV_POSTGRES_PASSWORD="${POSTGRES_PASSWORD:-}"
|
|
_ENV_CLICKHOUSE_PASSWORD="${CLICKHOUSE_PASSWORD:-}"
|
|
_ENV_TLS_MODE="${TLS_MODE:-}"
|
|
_ENV_CERT_FILE="${CERT_FILE:-}"
|
|
_ENV_KEY_FILE="${KEY_FILE:-}"
|
|
_ENV_CA_FILE="${CA_FILE:-}"
|
|
_ENV_HTTP_PORT="${HTTP_PORT:-}"
|
|
_ENV_HTTPS_PORT="${HTTPS_PORT:-}"
|
|
_ENV_LOGTO_CONSOLE_PORT="${LOGTO_CONSOLE_PORT:-}"
|
|
_ENV_LOGTO_CONSOLE_EXPOSED="${LOGTO_CONSOLE_EXPOSED:-}"
|
|
_ENV_VENDOR_ENABLED="${VENDOR_ENABLED:-}"
|
|
_ENV_VENDOR_USER="${VENDOR_USER:-}"
|
|
_ENV_VENDOR_PASS="${VENDOR_PASS:-}"
|
|
_ENV_MONITORING_NETWORK="${MONITORING_NETWORK:-}"
|
|
_ENV_COMPOSE_PROJECT="${COMPOSE_PROJECT:-}"
|
|
_ENV_DOCKER_SOCKET="${DOCKER_SOCKET:-}"
|
|
_ENV_NODE_TLS_REJECT="${NODE_TLS_REJECT:-}"
|
|
|
|
INSTALL_DIR=""
|
|
PUBLIC_HOST=""
|
|
PUBLIC_PROTOCOL=""
|
|
ADMIN_USER=""
|
|
ADMIN_PASS=""
|
|
TLS_MODE=""
|
|
CERT_FILE=""
|
|
KEY_FILE=""
|
|
CA_FILE=""
|
|
POSTGRES_PASSWORD=""
|
|
CLICKHOUSE_PASSWORD=""
|
|
HTTP_PORT=""
|
|
HTTPS_PORT=""
|
|
LOGTO_CONSOLE_PORT=""
|
|
LOGTO_CONSOLE_EXPOSED=""
|
|
VENDOR_ENABLED=""
|
|
VENDOR_USER=""
|
|
VENDOR_PASS=""
|
|
MONITORING_NETWORK=""
|
|
VERSION=""
|
|
COMPOSE_PROJECT=""
|
|
DOCKER_SOCKET=""
|
|
NODE_TLS_REJECT=""
|
|
|
|
# --- State ---
|
|
MODE="" # simple, expert, silent
|
|
IS_RERUN=false
|
|
RERUN_ACTION="" # upgrade, reconfigure, reinstall
|
|
CONFIRM_DESTROY=false
|
|
CONFIG_FILE_PATH=""
|
|
|
|
# --- Utility functions ---
|
|
|
|
log_info() { echo -e "${GREEN}[INFO]${NC} $1"; }
|
|
log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
|
|
log_error() { echo -e "${RED}[ERROR]${NC} $1"; }
|
|
log_success() { echo -e "${GREEN}[OK]${NC} $1"; }
|
|
|
|
print_banner() {
|
|
echo -e "${BOLD}"
|
|
echo " ____ _ "
|
|
echo " / ___|__ _ _ __ ___ ___ | | ___ ___ _ __ "
|
|
echo "| | / _\` | '_ \` _ \\ / _ \\| |/ _ \\/ _ \\ '__|"
|
|
echo "| |__| (_| | | | | | | __/| | __/ __/ | "
|
|
echo " \\____\\__,_|_| |_| |_|\\___||_|\\___|\\___||_| "
|
|
echo ""
|
|
echo " SaaS Platform Installer v${CAMELEER_INSTALLER_VERSION}"
|
|
echo -e "${NC}"
|
|
}
|
|
|
|
prompt() {
|
|
local var_name="$1" prompt_text="$2" default="${3:-}"
|
|
local input
|
|
if [ -n "$default" ]; then
|
|
read -rp " $prompt_text [$default]: " input
|
|
eval "$var_name=\"\${input:-$default}\""
|
|
else
|
|
read -rp " $prompt_text: " input
|
|
eval "$var_name=\"\$input\""
|
|
fi
|
|
}
|
|
|
|
prompt_password() {
|
|
local var_name="$1" prompt_text="$2" default="${3:-}"
|
|
local input
|
|
if [ -n "$default" ]; then
|
|
read -rsp " $prompt_text [${default:+********}]: " input
|
|
echo
|
|
eval "$var_name=\"\${input:-$default}\""
|
|
else
|
|
read -rsp " $prompt_text: " input
|
|
echo
|
|
eval "$var_name=\"\$input\""
|
|
fi
|
|
}
|
|
|
|
prompt_yesno() {
|
|
local prompt_text="$1" default="${2:-n}"
|
|
local input
|
|
if [ "$default" = "y" ]; then
|
|
read -rp " $prompt_text [Y/n]: " input
|
|
case "${input:-y}" in
|
|
[nN]|[nN][oO]) return 1 ;; *) return 0 ;; esac
|
|
else
|
|
read -rp " $prompt_text [y/N]: " input
|
|
case "${input:-n}" in
|
|
[yY]|[yY][eE][sS]) return 0 ;; *) return 1 ;; esac
|
|
fi
|
|
}
|
|
|
|
generate_password() {
|
|
openssl rand -base64 24 | tr -d '/+=' | head -c 32
|
|
}
|
|
|
|
# --- Argument parsing ---
|
|
|
|
parse_args() {
|
|
while [ $# -gt 0 ]; do
|
|
case "$1" in
|
|
--silent) MODE="silent" ;;
|
|
--expert) MODE="expert" ;;
|
|
--config) CONFIG_FILE_PATH="$2"; shift ;;
|
|
--install-dir) INSTALL_DIR="$2"; shift ;;
|
|
--public-host) PUBLIC_HOST="$2"; shift ;;
|
|
--public-protocol) PUBLIC_PROTOCOL="$2"; shift ;;
|
|
--admin-user) ADMIN_USER="$2"; shift ;;
|
|
--admin-password) ADMIN_PASS="$2"; shift ;;
|
|
--tls-mode) TLS_MODE="$2"; shift ;;
|
|
--cert-file) CERT_FILE="$2"; shift ;;
|
|
--key-file) KEY_FILE="$2"; shift ;;
|
|
--ca-file) CA_FILE="$2"; shift ;;
|
|
--postgres-password) POSTGRES_PASSWORD="$2"; shift ;;
|
|
--clickhouse-password) CLICKHOUSE_PASSWORD="$2"; shift ;;
|
|
--http-port) HTTP_PORT="$2"; shift ;;
|
|
--https-port) HTTPS_PORT="$2"; shift ;;
|
|
--logto-console-port) LOGTO_CONSOLE_PORT="$2"; shift ;;
|
|
--logto-console-exposed) LOGTO_CONSOLE_EXPOSED="$2"; shift ;;
|
|
--vendor-enabled) VENDOR_ENABLED="$2"; shift ;;
|
|
--vendor-user) VENDOR_USER="$2"; shift ;;
|
|
--vendor-password) VENDOR_PASS="$2"; shift ;;
|
|
--monitoring-network) MONITORING_NETWORK="$2"; shift ;;
|
|
--version) VERSION="$2"; shift ;;
|
|
--compose-project) COMPOSE_PROJECT="$2"; shift ;;
|
|
--docker-socket) DOCKER_SOCKET="$2"; shift ;;
|
|
--node-tls-reject) NODE_TLS_REJECT="$2"; shift ;;
|
|
--reconfigure) RERUN_ACTION="reconfigure" ;;
|
|
--reinstall) RERUN_ACTION="reinstall" ;;
|
|
--confirm-destroy) CONFIRM_DESTROY=true ;;
|
|
--help|-h) show_help; exit 0 ;;
|
|
*)
|
|
log_error "Unknown option: $1"
|
|
echo " Run with --help for usage."
|
|
exit 1
|
|
;;
|
|
esac
|
|
shift
|
|
done
|
|
}
|
|
|
|
show_help() {
|
|
echo "Usage: install.sh [OPTIONS]"
|
|
echo ""
|
|
echo "Modes:"
|
|
echo " (default) Interactive simple mode (6 questions)"
|
|
echo " --expert Interactive expert mode (all options)"
|
|
echo " --silent Non-interactive, use defaults + overrides"
|
|
echo ""
|
|
echo "Options:"
|
|
echo " --install-dir DIR Install directory (default: ./cameleer)"
|
|
echo " --public-host HOST Public hostname (default: auto-detect)"
|
|
echo " --admin-user USER Admin username (default: admin)"
|
|
echo " --admin-password PASS Admin password (default: generated)"
|
|
echo " --tls-mode MODE self-signed or custom (default: self-signed)"
|
|
echo " --cert-file PATH TLS certificate file"
|
|
echo " --key-file PATH TLS key file"
|
|
echo " --ca-file PATH CA bundle file"
|
|
echo " --monitoring-network NAME Docker network for Prometheus scraping"
|
|
echo " --version TAG Image version tag (default: latest)"
|
|
echo " --config FILE Load config from file"
|
|
echo " --help Show this help"
|
|
echo ""
|
|
echo "Expert options:"
|
|
echo " --postgres-password, --clickhouse-password, --http-port,"
|
|
echo " --https-port, --logto-console-port, --logto-console-exposed,"
|
|
echo " --vendor-enabled, --vendor-user, --vendor-password,"
|
|
echo " --compose-project, --docker-socket, --node-tls-reject"
|
|
echo ""
|
|
echo "Re-run options:"
|
|
echo " --reconfigure Re-run interactive setup (preserve data)"
|
|
echo " --reinstall --confirm-destroy Fresh install (destroys data)"
|
|
echo ""
|
|
echo "Config precedence: CLI flags > env vars > config file > defaults"
|
|
}
|
|
|
|
# --- Config file handling ---
|
|
|
|
load_config_file() {
|
|
local file="$1"
|
|
[ ! -f "$file" ] && return
|
|
while IFS='=' read -r key value; do
|
|
case "$key" in
|
|
\#*|"") continue ;;
|
|
esac
|
|
key=$(echo "$key" | tr -d ' ')
|
|
value=$(echo "$value" | sed 's/^[ ]*//;s/[ ]*$//')
|
|
case "$key" in
|
|
install_dir) [ -z "$INSTALL_DIR" ] && INSTALL_DIR="$value" ;;
|
|
public_host) [ -z "$PUBLIC_HOST" ] && PUBLIC_HOST="$value" ;;
|
|
public_protocol) [ -z "$PUBLIC_PROTOCOL" ] && PUBLIC_PROTOCOL="$value" ;;
|
|
admin_user) [ -z "$ADMIN_USER" ] && ADMIN_USER="$value" ;;
|
|
admin_password) [ -z "$ADMIN_PASS" ] && ADMIN_PASS="$value" ;;
|
|
tls_mode) [ -z "$TLS_MODE" ] && TLS_MODE="$value" ;;
|
|
cert_file) [ -z "$CERT_FILE" ] && CERT_FILE="$value" ;;
|
|
key_file) [ -z "$KEY_FILE" ] && KEY_FILE="$value" ;;
|
|
ca_file) [ -z "$CA_FILE" ] && CA_FILE="$value" ;;
|
|
postgres_password) [ -z "$POSTGRES_PASSWORD" ] && POSTGRES_PASSWORD="$value" ;;
|
|
clickhouse_password) [ -z "$CLICKHOUSE_PASSWORD" ] && CLICKHOUSE_PASSWORD="$value" ;;
|
|
http_port) [ -z "$HTTP_PORT" ] && HTTP_PORT="$value" ;;
|
|
https_port) [ -z "$HTTPS_PORT" ] && HTTPS_PORT="$value" ;;
|
|
logto_console_port) [ -z "$LOGTO_CONSOLE_PORT" ] && LOGTO_CONSOLE_PORT="$value" ;;
|
|
logto_console_exposed) [ -z "$LOGTO_CONSOLE_EXPOSED" ] && LOGTO_CONSOLE_EXPOSED="$value" ;;
|
|
vendor_enabled) [ -z "$VENDOR_ENABLED" ] && VENDOR_ENABLED="$value" ;;
|
|
vendor_user) [ -z "$VENDOR_USER" ] && VENDOR_USER="$value" ;;
|
|
vendor_password) [ -z "$VENDOR_PASS" ] && VENDOR_PASS="$value" ;;
|
|
monitoring_network) [ -z "$MONITORING_NETWORK" ] && MONITORING_NETWORK="$value" ;;
|
|
version) [ -z "$VERSION" ] && VERSION="$value" ;;
|
|
compose_project) [ -z "$COMPOSE_PROJECT" ] && COMPOSE_PROJECT="$value" ;;
|
|
docker_socket) [ -z "$DOCKER_SOCKET" ] && DOCKER_SOCKET="$value" ;;
|
|
node_tls_reject) [ -z "$NODE_TLS_REJECT" ] && NODE_TLS_REJECT="$value" ;;
|
|
esac
|
|
done < "$file"
|
|
}
|
|
|
|
load_env_overrides() {
|
|
[ -z "$INSTALL_DIR" ] && INSTALL_DIR="${CAMELEER_INSTALL_DIR:-}"
|
|
[ -z "$PUBLIC_HOST" ] && PUBLIC_HOST="$_ENV_PUBLIC_HOST"
|
|
[ -z "$PUBLIC_PROTOCOL" ] && PUBLIC_PROTOCOL="$_ENV_PUBLIC_PROTOCOL"
|
|
[ -z "$ADMIN_USER" ] && ADMIN_USER="${SAAS_ADMIN_USER:-}"
|
|
[ -z "$ADMIN_PASS" ] && ADMIN_PASS="${SAAS_ADMIN_PASS:-}"
|
|
[ -z "$TLS_MODE" ] && TLS_MODE="$_ENV_TLS_MODE"
|
|
[ -z "$CERT_FILE" ] && CERT_FILE="$_ENV_CERT_FILE"
|
|
[ -z "$KEY_FILE" ] && KEY_FILE="$_ENV_KEY_FILE"
|
|
[ -z "$CA_FILE" ] && CA_FILE="$_ENV_CA_FILE"
|
|
[ -z "$POSTGRES_PASSWORD" ] && POSTGRES_PASSWORD="$_ENV_POSTGRES_PASSWORD"
|
|
[ -z "$CLICKHOUSE_PASSWORD" ] && CLICKHOUSE_PASSWORD="$_ENV_CLICKHOUSE_PASSWORD"
|
|
[ -z "$HTTP_PORT" ] && HTTP_PORT="$_ENV_HTTP_PORT"
|
|
[ -z "$HTTPS_PORT" ] && HTTPS_PORT="$_ENV_HTTPS_PORT"
|
|
[ -z "$LOGTO_CONSOLE_PORT" ] && LOGTO_CONSOLE_PORT="$_ENV_LOGTO_CONSOLE_PORT"
|
|
[ -z "$LOGTO_CONSOLE_EXPOSED" ] && LOGTO_CONSOLE_EXPOSED="$_ENV_LOGTO_CONSOLE_EXPOSED"
|
|
[ -z "$VENDOR_ENABLED" ] && VENDOR_ENABLED="$_ENV_VENDOR_ENABLED"
|
|
[ -z "$VENDOR_USER" ] && VENDOR_USER="$_ENV_VENDOR_USER"
|
|
[ -z "$VENDOR_PASS" ] && VENDOR_PASS="$_ENV_VENDOR_PASS"
|
|
[ -z "$MONITORING_NETWORK" ] && MONITORING_NETWORK="$_ENV_MONITORING_NETWORK"
|
|
[ -z "$VERSION" ] && VERSION="${CAMELEER_VERSION:-}"
|
|
[ -z "$COMPOSE_PROJECT" ] && COMPOSE_PROJECT="$_ENV_COMPOSE_PROJECT"
|
|
[ -z "$DOCKER_SOCKET" ] && DOCKER_SOCKET="$_ENV_DOCKER_SOCKET"
|
|
[ -z "$NODE_TLS_REJECT" ] && NODE_TLS_REJECT="$_ENV_NODE_TLS_REJECT"
|
|
}
|
|
|
|
# --- Prerequisites ---
|
|
|
|
check_prerequisites() {
|
|
log_info "Checking prerequisites..."
|
|
local errors=0
|
|
|
|
if ! command -v docker >/dev/null 2>&1; then
|
|
log_error "Docker is not installed."
|
|
echo " Install Docker Engine: https://docs.docker.com/engine/install/"
|
|
errors=$((errors + 1))
|
|
else
|
|
local docker_version
|
|
docker_version=$(docker version --format '{{.Server.Version}}' 2>/dev/null || echo "unknown")
|
|
log_info "Docker version: $docker_version"
|
|
fi
|
|
|
|
if ! docker compose version >/dev/null 2>&1; then
|
|
log_error "Docker Compose v2 is not available."
|
|
echo " 'docker compose' subcommand required (not standalone docker-compose)."
|
|
errors=$((errors + 1))
|
|
else
|
|
local compose_version
|
|
compose_version=$(docker compose version --short 2>/dev/null || echo "unknown")
|
|
log_info "Docker Compose version: $compose_version"
|
|
fi
|
|
|
|
if ! command -v openssl >/dev/null 2>&1; then
|
|
log_error "OpenSSL is not installed (needed for password generation)."
|
|
errors=$((errors + 1))
|
|
fi
|
|
|
|
local socket="${DOCKER_SOCKET:-$DEFAULT_DOCKER_SOCKET}"
|
|
if [ ! -S "$socket" ]; then
|
|
log_warn "Docker socket not found at $socket"
|
|
fi
|
|
|
|
check_port_available "${HTTP_PORT:-$DEFAULT_HTTP_PORT}" "HTTP"
|
|
check_port_available "${HTTPS_PORT:-$DEFAULT_HTTPS_PORT}" "HTTPS"
|
|
check_port_available "${LOGTO_CONSOLE_PORT:-$DEFAULT_LOGTO_CONSOLE_PORT}" "Logto Console"
|
|
|
|
if [ $errors -gt 0 ]; then
|
|
log_error "$errors prerequisite(s) not met. Please install missing dependencies and retry."
|
|
exit 1
|
|
fi
|
|
log_success "All prerequisites met."
|
|
}
|
|
|
|
check_port_available() {
|
|
local port="$1" name="$2"
|
|
if command -v ss >/dev/null 2>&1; then
|
|
if ss -tlnp 2>/dev/null | grep -q ":${port} "; then
|
|
log_warn "Port $port ($name) is already in use."
|
|
fi
|
|
elif command -v netstat >/dev/null 2>&1; then
|
|
if netstat -tlnp 2>/dev/null | grep -q ":${port} "; then
|
|
log_warn "Port $port ($name) is already in use."
|
|
fi
|
|
fi
|
|
}
|
|
|
|
# --- Auto-detection ---
|
|
|
|
auto_detect() {
|
|
if [ -z "$PUBLIC_HOST" ]; then
|
|
PUBLIC_HOST=$(hostname -f 2>/dev/null || hostname 2>/dev/null || echo "localhost")
|
|
local primary_ip
|
|
primary_ip=$(ip route get 1.1.1.1 2>/dev/null | awk '{print $7; exit}' || true)
|
|
if [ -n "$primary_ip" ]; then
|
|
local rdns
|
|
rdns=$(dig +short -x "$primary_ip" 2>/dev/null | sed 's/\.$//' || true)
|
|
[ -n "$rdns" ] && PUBLIC_HOST="$rdns"
|
|
fi
|
|
fi
|
|
|
|
if [ -z "$DOCKER_SOCKET" ]; then
|
|
DOCKER_SOCKET="$DEFAULT_DOCKER_SOCKET"
|
|
fi
|
|
}
|
|
|
|
detect_existing_install() {
|
|
local dir="${INSTALL_DIR:-$DEFAULT_INSTALL_DIR}"
|
|
if [ -f "$dir/cameleer.conf" ]; then
|
|
IS_RERUN=true
|
|
INSTALL_DIR="$dir"
|
|
load_config_file "$dir/cameleer.conf"
|
|
fi
|
|
}
|