mirror of
https://github.com/m1ngsama/automa.git
synced 2026-03-25 18:23:49 +00:00
feat: add interactive setup.sh wizard
Discovers all deployable modules from services/ automatically. Grouped menu by role (vps / homeserver / any) with descriptions. Env resolution priority: 1. pre-filled .env in local infra checkout (--infra-dir) 2. .env.example from infra (interactive fill) 3. .env.example bundled in automa (interactive fill, no infra needed) Usage: ./setup.sh # fully interactive ./setup.sh --infra-dir /path/to/infra # use pre-filled .env files ./setup.sh --dry-run # preview without deploying Also add .env.example with role/description metadata to each service module so setup.sh can build the menu and prompt for values without requiring an infra checkout.
This commit is contained in:
parent
50ecd7c814
commit
1356348d79
7 changed files with 377 additions and 0 deletions
7
services/email/.env.example
Normal file
7
services/email/.env.example
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
# role: vps
|
||||||
|
# description: Postfix + Dovecot + OpenDKIM + SpamAssassin full email stack
|
||||||
|
|
||||||
|
DOMAIN=your-domain.com
|
||||||
|
MAIL_HOST=mail.your-domain.com
|
||||||
|
SERVER_IP=x.x.x.x
|
||||||
|
MAIL_USER=contact
|
||||||
6
services/frp/client/.env.example
Normal file
6
services/frp/client/.env.example
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
# role: homeserver
|
||||||
|
# description: FRP client (frpc) — tunnels local services through VPS
|
||||||
|
|
||||||
|
FRP_SERVER_ADDR=your-vps-ip
|
||||||
|
FRP_SERVER_PORT=7000
|
||||||
|
FRP_TOKEN=
|
||||||
6
services/frp/server/.env.example
Normal file
6
services/frp/server/.env.example
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
# role: vps
|
||||||
|
# description: FRP server (frps) — public entry point for reverse tunnels
|
||||||
|
|
||||||
|
FRP_TOKEN=
|
||||||
|
FRP_WEB_PASSWORD=
|
||||||
|
FRP_BIND_PORT=7000
|
||||||
8
services/nginx/.env.example
Normal file
8
services/nginx/.env.example
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
# role: vps
|
||||||
|
# description: Nginx web server and reverse proxy vhosts
|
||||||
|
|
||||||
|
DOMAIN=your-domain.com
|
||||||
|
BLOG_DOMAIN=blog.your-domain.com
|
||||||
|
CHAN_DOMAIN=chan.your-domain.com
|
||||||
|
MAIL_DOMAIN=mail.your-domain.com
|
||||||
|
GIT_DOMAIN=git.your-domain.com
|
||||||
7
services/shadowsocks/client/.env.example
Normal file
7
services/shadowsocks/client/.env.example
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
# role: homeserver
|
||||||
|
# description: sslocal + privoxy proxy chain (SOCKS5 :1080, HTTP :8118)
|
||||||
|
|
||||||
|
SS_SERVER=your-vps-ip
|
||||||
|
SS_PORT=41268
|
||||||
|
SS_PASSWORD=
|
||||||
|
SS_METHOD=2022-blake3-aes-256-gcm
|
||||||
6
services/shadowsocks/server/.env.example
Normal file
6
services/shadowsocks/server/.env.example
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
# role: vps
|
||||||
|
# description: Shadowsocks-Rust server (GFW-resistant proxy)
|
||||||
|
|
||||||
|
SS_PORT=41268
|
||||||
|
SS_PASSWORD=
|
||||||
|
SS_METHOD=2022-blake3-aes-256-gcm
|
||||||
337
setup.sh
Executable file
337
setup.sh
Executable file
|
|
@ -0,0 +1,337 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# Interactive installer for infra services.
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# ./setup.sh # fully interactive
|
||||||
|
# ./setup.sh --infra-dir PATH # use pre-filled .env files from local infra checkout
|
||||||
|
# ./setup.sh --dry-run # show what would be deployed, no changes
|
||||||
|
# INFRA_DIR=/path/to/infra ./setup.sh # same as --infra-dir via env var
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
source "$SCRIPT_DIR/bin/lib/common.sh"
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Args
|
||||||
|
# ============================================================================
|
||||||
|
INFRA_DIR="${INFRA_DIR:-}"
|
||||||
|
DRY_RUN=0
|
||||||
|
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--infra-dir) INFRA_DIR="$2"; shift 2 ;;
|
||||||
|
--dry-run) DRY_RUN=1; shift ;;
|
||||||
|
*) log_error "Unknown argument: $1"; exit 1 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Banner
|
||||||
|
# ============================================================================
|
||||||
|
echo ""
|
||||||
|
echo " ┌─────────────────────────────────────┐"
|
||||||
|
echo " │ automa setup │"
|
||||||
|
echo " │ interactive infrastructure deploy │"
|
||||||
|
echo " └─────────────────────────────────────┘"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Prerequisites (skipped in dry-run)
|
||||||
|
# ============================================================================
|
||||||
|
check_prerequisites() {
|
||||||
|
log_info "Checking prerequisites..."
|
||||||
|
local missing=0
|
||||||
|
for cmd in curl wget systemctl envsubst; do
|
||||||
|
if ! command -v "$cmd" &>/dev/null; then
|
||||||
|
log_warn " missing: $cmd"
|
||||||
|
missing=1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
if [[ "$missing" -eq 1 ]]; then
|
||||||
|
log_error "Install missing tools before continuing."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
log_info "Prerequisites OK."
|
||||||
|
}
|
||||||
|
|
||||||
|
[[ "$DRY_RUN" -eq 0 ]] && check_prerequisites
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Infra dir resolution
|
||||||
|
# ============================================================================
|
||||||
|
if [[ -n "$INFRA_DIR" ]]; then
|
||||||
|
if [[ ! -d "$INFRA_DIR" ]]; then
|
||||||
|
log_error "INFRA_DIR does not exist: $INFRA_DIR"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
log_info "Using infra dir: $INFRA_DIR"
|
||||||
|
else
|
||||||
|
echo ""
|
||||||
|
echo " No --infra-dir specified."
|
||||||
|
echo " Point to a local infra checkout (with pre-filled .env files) for auto-config,"
|
||||||
|
echo " or leave blank to enter all values interactively."
|
||||||
|
echo ""
|
||||||
|
read -rp " Path to local infra repo [blank = interactive mode]: " infra_input
|
||||||
|
if [[ -n "$infra_input" ]]; then
|
||||||
|
infra_input="${infra_input/#\~/$HOME}"
|
||||||
|
if [[ -d "$infra_input" ]]; then
|
||||||
|
INFRA_DIR="$infra_input"
|
||||||
|
log_info "Using infra dir: $INFRA_DIR"
|
||||||
|
else
|
||||||
|
log_warn "Directory not found, continuing in interactive mode."
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Module discovery
|
||||||
|
# Parallel arrays indexed by position: MODULES[i], ROLES[i], DESCS[i]
|
||||||
|
# MENU_MODS[menu_number] = module_path (1-based; index 0 = "")
|
||||||
|
# ============================================================================
|
||||||
|
MODULES=()
|
||||||
|
ROLES=()
|
||||||
|
DESCS=()
|
||||||
|
MENU_MODS=("") # index 0 unused — menu numbers start at 1
|
||||||
|
|
||||||
|
discover_modules() {
|
||||||
|
while IFS= read -r deploy; do
|
||||||
|
local mod env_ex role desc r d
|
||||||
|
mod="$(dirname "$deploy" | sed "s|^$SCRIPT_DIR/services/||")"
|
||||||
|
env_ex="$SCRIPT_DIR/services/$mod/.env.example"
|
||||||
|
|
||||||
|
role="any"
|
||||||
|
desc="$mod"
|
||||||
|
|
||||||
|
if [[ -f "$env_ex" ]]; then
|
||||||
|
# grep returns 1 if no match — use || true to avoid pipefail exit
|
||||||
|
r="$(grep '^# role:' "$env_ex" | head -1 | sed 's/^# role: *//' || true)"
|
||||||
|
d="$(grep '^# description:' "$env_ex" | head -1 | sed 's/^# description: *//' || true)"
|
||||||
|
[[ -n "$r" ]] && role="$r"
|
||||||
|
[[ -n "$d" ]] && desc="$d"
|
||||||
|
fi
|
||||||
|
|
||||||
|
MODULES+=("$mod")
|
||||||
|
ROLES+=("$role")
|
||||||
|
DESCS+=("$desc")
|
||||||
|
done < <(find "$SCRIPT_DIR/services" -name "deploy.sh" | sort)
|
||||||
|
}
|
||||||
|
|
||||||
|
discover_modules
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Interactive menu (grouped by role)
|
||||||
|
# Populates MENU_MODS as a side effect.
|
||||||
|
# ============================================================================
|
||||||
|
print_menu() {
|
||||||
|
local printed_any=0
|
||||||
|
|
||||||
|
for role_group in "vps" "homeserver" "any"; do
|
||||||
|
local label header_printed=0
|
||||||
|
case "$role_group" in
|
||||||
|
vps) label="VPS services" ;;
|
||||||
|
homeserver) label="Home server services" ;;
|
||||||
|
any) label="Any machine" ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
for i in "${!MODULES[@]}"; do
|
||||||
|
[[ "${ROLES[$i]}" != "$role_group" ]] && continue
|
||||||
|
|
||||||
|
if [[ "$header_printed" -eq 0 ]]; then
|
||||||
|
echo " $label:"
|
||||||
|
header_printed=1
|
||||||
|
printed_any=1
|
||||||
|
fi
|
||||||
|
|
||||||
|
local menu_idx=${#MENU_MODS[@]}
|
||||||
|
MENU_MODS+=("${MODULES[$i]}")
|
||||||
|
printf " [%2d] %-30s %s\n" "$menu_idx" "${MODULES[$i]}" "${DESCS[$i]}"
|
||||||
|
done
|
||||||
|
|
||||||
|
[[ "$header_printed" -eq 1 ]] && echo ""
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ "$printed_any" -eq 0 ]]; then
|
||||||
|
log_error "No deployable modules found."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
print_menu
|
||||||
|
|
||||||
|
echo " Enter module numbers to deploy (space-separated, e.g. \"1 3\")."
|
||||||
|
echo " Press Enter to select all, or type \"none\" to exit."
|
||||||
|
echo ""
|
||||||
|
read -rp " > " selection
|
||||||
|
|
||||||
|
if [[ "$selection" == "none" ]]; then
|
||||||
|
echo " Exiting."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
SELECTED_MODULES=()
|
||||||
|
|
||||||
|
if [[ -z "$selection" ]]; then
|
||||||
|
SELECTED_MODULES=("${MODULES[@]}")
|
||||||
|
else
|
||||||
|
for num in $selection; do
|
||||||
|
if [[ "$num" =~ ^[0-9]+$ ]] \
|
||||||
|
&& [[ "$num" -gt 0 ]] \
|
||||||
|
&& [[ "$num" -lt "${#MENU_MODS[@]}" ]]; then
|
||||||
|
SELECTED_MODULES+=("${MENU_MODS[$num]}")
|
||||||
|
else
|
||||||
|
log_warn "Unknown module number: $num (skipped)"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ ${#SELECTED_MODULES[@]} -eq 0 ]]; then
|
||||||
|
log_error "No valid modules selected."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
log_info "Selected: ${SELECTED_MODULES[*]}"
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Env resolution
|
||||||
|
# Priority:
|
||||||
|
# 1. $INFRA_DIR/services/$mod/.env (pre-filled, use as-is)
|
||||||
|
# 2. $INFRA_DIR/services/$mod/.env.example (prompt, fill from infra template)
|
||||||
|
# 3. $SCRIPT_DIR/services/$mod/.env.example (prompt, fill from automa template)
|
||||||
|
# Prints the resolved .env path to stdout.
|
||||||
|
# ============================================================================
|
||||||
|
fill_env_interactive() {
|
||||||
|
# All user-visible output → stderr so caller can capture stdout for the path.
|
||||||
|
local env_example="$1"
|
||||||
|
local output="$2"
|
||||||
|
|
||||||
|
printf "" > "$output"
|
||||||
|
echo " Fill in values (Enter = accept default shown in [brackets]):" >&2
|
||||||
|
echo "" >&2
|
||||||
|
|
||||||
|
while IFS= read -r line; do
|
||||||
|
if [[ "$line" =~ ^#\ (role|description): ]]; then
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
if [[ -z "$line" ]]; then
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
if [[ "$line" =~ ^# ]]; then
|
||||||
|
printf " %s\n" "$line" >&2
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
local key default hint val
|
||||||
|
key="${line%%=*}"
|
||||||
|
default="${line#*=}"
|
||||||
|
hint=""
|
||||||
|
|
||||||
|
if [[ -n "$default" && "$default" != "your-"* && "$default" != "x.x.x.x" ]]; then
|
||||||
|
hint=" [$default]"
|
||||||
|
fi
|
||||||
|
|
||||||
|
read -rp " $key$hint: " val <&0
|
||||||
|
printf "%s=%s\n" "$key" "${val:-$default}" >> "$output"
|
||||||
|
done < "$env_example"
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve_env_for_module() {
|
||||||
|
# Prints resolved .env path to stdout; all messages → stderr.
|
||||||
|
local mod="$1"
|
||||||
|
local dry_run="${2:-0}"
|
||||||
|
local infra_mod_dir=""
|
||||||
|
|
||||||
|
if [[ -n "$INFRA_DIR" ]]; then
|
||||||
|
infra_mod_dir="$INFRA_DIR/services/$mod"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Case 1: pre-filled .env in infra — use directly
|
||||||
|
if [[ -n "$infra_mod_dir" && -f "$infra_mod_dir/.env" ]]; then
|
||||||
|
log_info " Using pre-filled .env from infra" >&2
|
||||||
|
printf "%s" "$infra_mod_dir/.env"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Determine .env.example template source
|
||||||
|
local env_example=""
|
||||||
|
if [[ -n "$infra_mod_dir" && -f "$infra_mod_dir/.env.example" ]]; then
|
||||||
|
env_example="$infra_mod_dir/.env.example"
|
||||||
|
elif [[ -f "$SCRIPT_DIR/services/$mod/.env.example" ]]; then
|
||||||
|
env_example="$SCRIPT_DIR/services/$mod/.env.example"
|
||||||
|
else
|
||||||
|
log_error " No .env.example found for: $mod" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Dry-run: skip interactive fill, just return path to the example
|
||||||
|
if [[ "$dry_run" -eq 1 ]]; then
|
||||||
|
printf "%s" "$env_example"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
local tmp_env
|
||||||
|
tmp_env="$(mktemp /tmp/automa-env-XXXXXX)"
|
||||||
|
fill_env_interactive "$env_example" "$tmp_env"
|
||||||
|
printf "%s" "$tmp_env"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Deployment
|
||||||
|
# ============================================================================
|
||||||
|
DEPLOYED=()
|
||||||
|
FAILED=()
|
||||||
|
|
||||||
|
for mod in "${SELECTED_MODULES[@]}"; do
|
||||||
|
echo ""
|
||||||
|
echo " ── $mod ──"
|
||||||
|
|
||||||
|
local_env=""
|
||||||
|
if ! local_env="$(resolve_env_for_module "$mod" "$DRY_RUN")"; then
|
||||||
|
FAILED+=("$mod")
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
deploy_script="$SCRIPT_DIR/services/$mod/deploy.sh"
|
||||||
|
if [[ ! -x "$deploy_script" ]]; then
|
||||||
|
log_error " deploy.sh not found or not executable: $deploy_script"
|
||||||
|
FAILED+=("$mod")
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$DRY_RUN" -eq 1 ]]; then
|
||||||
|
log_info " [dry-run] $mod → $deploy_script (env: $local_env)"
|
||||||
|
DEPLOYED+=("$mod")
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
if INFRA_DIR="$(dirname "$local_env")" bash "$deploy_script"; then
|
||||||
|
DEPLOYED+=("$mod")
|
||||||
|
else
|
||||||
|
log_error " Deployment failed: $mod"
|
||||||
|
FAILED+=("$mod")
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Summary
|
||||||
|
# ============================================================================
|
||||||
|
echo ""
|
||||||
|
echo " ┌─────────────────────────────────────┐"
|
||||||
|
echo " │ Summary │"
|
||||||
|
echo " └─────────────────────────────────────┘"
|
||||||
|
|
||||||
|
if [[ ${#DEPLOYED[@]} -gt 0 ]]; then
|
||||||
|
echo ""
|
||||||
|
log_info "Deployed (${#DEPLOYED[@]}):"
|
||||||
|
for m in "${DEPLOYED[@]}"; do echo " ✓ $m"; done
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ ${#FAILED[@]} -gt 0 ]]; then
|
||||||
|
echo ""
|
||||||
|
log_error "Failed (${#FAILED[@]}):"
|
||||||
|
for m in "${FAILED[@]}"; do echo " ✗ $m"; done
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
log_info "Done."
|
||||||
Loading…
Reference in a new issue