373 lines
12 KiB
Bash
Executable File
373 lines
12 KiB
Bash
Executable File
#!/usr/bin/env bash
|
||
# ─────────────────────────────────────────────────────────────
|
||
# Caddy Manager – gum-powered TUI
|
||
# SSH target : root@192.168.0.17
|
||
# Sites dir : ~/sites
|
||
# ─────────────────────────────────────────────────────────────
|
||
|
||
set -uo pipefail
|
||
|
||
SSH_TARGET="root@192.168.0.17"
|
||
SITES_DIR="~/sites"
|
||
RELOAD_CMD="docker exec -w /etc/caddy caddy caddy reload"
|
||
|
||
# ── Colour palette ────────────────────────────────────────────
|
||
ORANGE="#FF6B00"
|
||
ORANGE_DIM="#CC5500"
|
||
GREY_DARK="#2A2A2A"
|
||
GREY_MID="#4A4A4A"
|
||
GREY_LIGHT="#AAAAAA"
|
||
WHITE="#F5F5F5"
|
||
|
||
# ── Dependency check ─────────────────────────────────────────
|
||
if ! command -v gum &>/dev/null; then
|
||
echo "ERROR: 'gum' is not installed."
|
||
exit 1
|
||
fi
|
||
|
||
if ! command -v ssh &>/dev/null; then
|
||
echo "ERROR: 'ssh' is not installed."
|
||
exit 1
|
||
fi
|
||
|
||
# ── Helpers ───────────────────────────────────────────────────
|
||
|
||
draw_title() {
|
||
clear
|
||
gum style \
|
||
--foreground "$ORANGE" \
|
||
--border-foreground "$ORANGE" \
|
||
--border double \
|
||
--align center \
|
||
--width 50 \
|
||
--margin "1 2" \
|
||
--padding "1 4" \
|
||
--bold \
|
||
"⚙ CADDY MANAGER ⚙" \
|
||
"" \
|
||
"$(gum style --foreground "$GREY_LIGHT" --faint "$(date '+%a %d %b %Y %H:%M')")"
|
||
}
|
||
|
||
ssh_run() {
|
||
# Run a command on the remote host
|
||
ssh -o ConnectTimeout=10 -o BatchMode=yes "$SSH_TARGET" "$@"
|
||
}
|
||
|
||
reload_caddy() {
|
||
gum spin \
|
||
--spinner dot \
|
||
--title "Reloading Caddy..." \
|
||
--title.foreground "$ORANGE" \
|
||
-- ssh -o ConnectTimeout=10 -o BatchMode=yes "$SSH_TARGET" "$RELOAD_CMD"
|
||
}
|
||
|
||
press_enter() {
|
||
echo ""
|
||
gum style --foreground "$GREY_LIGHT" "Press Enter to return to the menu..."
|
||
read -r
|
||
}
|
||
|
||
# ── Option 1 – Add a new config ───────────────────────────────
|
||
|
||
add_config() {
|
||
draw_title
|
||
gum style \
|
||
--foreground "$ORANGE" \
|
||
--bold \
|
||
--margin "0 2" \
|
||
" ADD NEW SITE CONFIG"
|
||
echo ""
|
||
|
||
# Service name (used as filename)
|
||
SERVICE=$(gum input \
|
||
--prompt " Service name (filename): " \
|
||
--prompt.foreground "$ORANGE" \
|
||
--placeholder "e.g. nextcloud" \
|
||
--width 40 \
|
||
--cursor.foreground "$ORANGE")
|
||
|
||
[[ -z "$SERVICE" ]] && { gum style --foreground "red" " Aborted – no service name given."; press_enter; return; }
|
||
|
||
# Domain – type subdomain only, suffix appended automatically
|
||
SUBDOMAIN=$(gum input \
|
||
--prompt " Subdomain: " \
|
||
--prompt.foreground "$ORANGE" \
|
||
--placeholder "e.g. homarr (will become homarr.marlow.quest)" \
|
||
--width 50 \
|
||
--cursor.foreground "$ORANGE")
|
||
|
||
[[ -z "$SUBDOMAIN" ]] && { gum style --foreground "red" " Aborted – no domain given."; press_enter; return; }
|
||
|
||
# If they typed a full domain (contains a dot) use as-is, otherwise append suffix
|
||
if [[ "$SUBDOMAIN" == *.* ]]; then
|
||
DOMAIN="$SUBDOMAIN"
|
||
else
|
||
DOMAIN="${SUBDOMAIN}.marlow.quest"
|
||
fi
|
||
|
||
gum style --foreground "$GREY_LIGHT" --margin "0 2" " → ${DOMAIN}"
|
||
echo ""
|
||
|
||
# Upstream IP:port – pre-filled with common subnet prefix
|
||
UPSTREAM=$(gum input \
|
||
--prompt " Upstream IP:port: " \
|
||
--prompt.foreground "$ORANGE" \
|
||
--value "192.168.0." \
|
||
--width 40 \
|
||
--cursor.foreground "$ORANGE")
|
||
|
||
[[ -z "$UPSTREAM" ]] && { gum style --foreground "red" " Aborted – no upstream given."; press_enter; return; }
|
||
|
||
# TinyAuth?
|
||
echo ""
|
||
gum style --foreground "$GREY_LIGHT" --margin "0 2" " Include TinyAuth middleware?"
|
||
USE_TINYAUTH=$(gum choose \
|
||
--cursor "▶ " \
|
||
--cursor.foreground "$ORANGE" \
|
||
--selected.foreground "$ORANGE" \
|
||
--header " Select an option:" \
|
||
--header.foreground "$GREY_LIGHT" \
|
||
"Yes" "No")
|
||
|
||
# Build the Caddy config block
|
||
if [[ "$USE_TINYAUTH" == "Yes" ]]; then
|
||
CONFIG_BLOCK="${DOMAIN} {
|
||
import tinyauth
|
||
|
||
reverse_proxy ${UPSTREAM}
|
||
}"
|
||
else
|
||
CONFIG_BLOCK="${DOMAIN} {
|
||
reverse_proxy ${UPSTREAM}
|
||
}"
|
||
fi
|
||
|
||
# Preview
|
||
echo ""
|
||
gum style \
|
||
--foreground "$ORANGE" \
|
||
--border normal \
|
||
--border-foreground "$GREY_MID" \
|
||
--padding "1 2" \
|
||
--margin "0 2" \
|
||
"$CONFIG_BLOCK"
|
||
echo ""
|
||
|
||
gum confirm \
|
||
--prompt.foreground "$ORANGE" \
|
||
--selected.background "$ORANGE" \
|
||
--selected.foreground "$GREY_DARK" \
|
||
" Write ${SERVICE}.conf to ${SITES_DIR} and reload Caddy?" \
|
||
|| { gum style --foreground "$GREY_LIGHT" " Cancelled."; press_enter; return; }
|
||
|
||
# Write file via SSH heredoc
|
||
ssh_run "cat > ${SITES_DIR}/${SERVICE}.conf" <<EOF
|
||
${CONFIG_BLOCK}
|
||
EOF
|
||
|
||
echo ""
|
||
gum style --foreground "green" " ✔ Config written to ${SITES_DIR}/${SERVICE}.conf"
|
||
|
||
reload_caddy && gum style --foreground "green" " ✔ Caddy reloaded successfully." \
|
||
|| gum style --foreground "red" " ✗ Caddy reload failed – check logs."
|
||
press_enter
|
||
}
|
||
|
||
# ── Option 2 – List / view configs ───────────────────────────
|
||
|
||
list_configs() {
|
||
while true; do
|
||
draw_title
|
||
gum style \
|
||
--foreground "$ORANGE" \
|
||
--bold \
|
||
--margin "0 2" \
|
||
" SITE CONFIGS"
|
||
echo ""
|
||
|
||
# Fetch all file names
|
||
mapfile -t FILES < <(ssh_run "ls ${SITES_DIR}/" 2>/dev/null || true)
|
||
|
||
# Remove blank entries
|
||
CLEAN_FILES=()
|
||
for f in "${FILES[@]+"${FILES[@]}"}"; do
|
||
[[ -n "$f" ]] && CLEAN_FILES+=("$f")
|
||
done
|
||
|
||
if [[ ${#CLEAN_FILES[@]} -eq 0 ]]; then
|
||
gum style --foreground "$GREY_LIGHT" --margin "0 2" \
|
||
" No config files found in ${SITES_DIR}/"
|
||
press_enter
|
||
return
|
||
fi
|
||
|
||
# Parse each file individually — build display arrays and a lookup map
|
||
T_NAME=()
|
||
T_DOMAIN=()
|
||
T_UPSTREAM=()
|
||
T_TINYAUTH=()
|
||
|
||
for FILE in "${CLEAN_FILES[@]}"; do
|
||
FILE_CONTENT=$(ssh_run "cat ${SITES_DIR}/${FILE}" 2>/dev/null || true)
|
||
|
||
DOMAIN=$(echo "$FILE_CONTENT" \
|
||
| grep -v '^\s*#' | grep -v '^\s*$' | grep -v '^\s' \
|
||
| grep '\.' | head -1 | awk '{print $1}' | tr -d '{}' || true)
|
||
[[ -z "$DOMAIN" ]] && DOMAIN="—"
|
||
|
||
UPSTREAM=$(echo "$FILE_CONTENT" \
|
||
| grep -i 'reverse_proxy' | awk '{print $2}' | head -1 || true)
|
||
[[ -z "$UPSTREAM" ]] && UPSTREAM="—"
|
||
|
||
if echo "$FILE_CONTENT" | grep -qi 'tinyauth' 2>/dev/null; then
|
||
TA="yes"
|
||
else
|
||
TA="no"
|
||
fi
|
||
|
||
T_NAME+=("$FILE")
|
||
T_DOMAIN+=("$DOMAIN")
|
||
T_UPSTREAM+=("$UPSTREAM")
|
||
T_TINYAUTH+=("$TA")
|
||
done
|
||
|
||
# Write CSV to temp file (gum table needs a real fd, not a pipe)
|
||
TMPCSV=$(mktemp /tmp/caddy-XXXXXX.csv)
|
||
for i in "${!T_NAME[@]}"; do
|
||
printf '%s,%s,%s,%s\n' \
|
||
"${T_NAME[$i]}" "${T_DOMAIN[$i]}" "${T_UPSTREAM[$i]}" "${T_TINYAUTH[$i]}" \
|
||
>> "$TMPCSV"
|
||
done
|
||
|
||
gum style --foreground "$GREY_LIGHT" --margin "0 2" \
|
||
" ↑↓ navigate · Enter to view full config · Esc to go back"
|
||
echo ""
|
||
|
||
SELECTED=$(gum table \
|
||
--columns "FILE,DOMAIN,UPSTREAM,TINYAUTH" \
|
||
--widths "26,30,22,9" \
|
||
--height 18 \
|
||
--border.foreground "$ORANGE" \
|
||
--header.foreground "$ORANGE" \
|
||
--selected.foreground "$GREY_DARK" \
|
||
--selected.background "$ORANGE" \
|
||
< "$TMPCSV" || true)
|
||
|
||
rm -f "$TMPCSV"
|
||
|
||
[[ -z "$SELECTED" ]] && return
|
||
|
||
# Extract filename from first CSV column
|
||
CHOICE=$(echo "$SELECTED" | cut -d',' -f1 | xargs)
|
||
|
||
# ── Show full file contents ────────────────────────────
|
||
clear
|
||
gum style \
|
||
--foreground "$ORANGE" \
|
||
--bold \
|
||
--margin "1 2 0 2" \
|
||
" ▸ ${CHOICE}"
|
||
echo ""
|
||
|
||
CONTENT=$(ssh_run "cat ${SITES_DIR}/${CHOICE}" 2>/dev/null || true)
|
||
[[ -z "$CONTENT" ]] && CONTENT="(unable to read file)"
|
||
|
||
gum style \
|
||
--foreground "$WHITE" \
|
||
--border normal \
|
||
--border-foreground "$GREY_MID" \
|
||
--padding "1 2" \
|
||
--margin "0 2" \
|
||
"$CONTENT"
|
||
|
||
echo ""
|
||
press_enter
|
||
done
|
||
}
|
||
|
||
# ── Option 3 – Delete a config ────────────────────────────────
|
||
|
||
delete_config() {
|
||
draw_title
|
||
gum style \
|
||
--foreground "$ORANGE" \
|
||
--bold \
|
||
--margin "0 2" \
|
||
" DELETE SITE CONFIG"
|
||
echo ""
|
||
|
||
# Fetch file list
|
||
mapfile -t FILES < <(ssh_run "ls ${SITES_DIR}/" 2>/dev/null | sort)
|
||
|
||
if [[ ${#FILES[@]} -eq 0 ]]; then
|
||
gum style --foreground "$GREY_LIGHT" --margin "0 2" " No config files found in ${SITES_DIR}/"
|
||
press_enter
|
||
return
|
||
fi
|
||
|
||
CHOICE=$(printf '%s\n' "${FILES[@]}" "── Cancel ──" | \
|
||
gum choose \
|
||
--cursor "▶ " \
|
||
--cursor.foreground "$ORANGE" \
|
||
--selected.foreground "red" \
|
||
--header " Select a config to delete:" \
|
||
--header.foreground "$GREY_LIGHT" \
|
||
--height 15)
|
||
|
||
[[ "$CHOICE" == "── Cancel ──" || -z "$CHOICE" ]] && { gum style --foreground "$GREY_LIGHT" " Cancelled."; press_enter; return; }
|
||
|
||
echo ""
|
||
|
||
# Show contents before confirming
|
||
CONTENT=$(ssh_run "cat ${SITES_DIR}/${CHOICE}" 2>/dev/null || echo "(unable to read file)")
|
||
gum style \
|
||
--foreground "$GREY_LIGHT" \
|
||
--border normal \
|
||
--border-foreground "red" \
|
||
--padding "1 2" \
|
||
--margin "0 2" \
|
||
"$CONTENT"
|
||
echo ""
|
||
|
||
gum confirm \
|
||
--prompt.foreground "$ORANGE" \
|
||
--selected.background "$ORANGE" \
|
||
--selected.foreground "$GREY_DARK" \
|
||
" Permanently delete ${CHOICE} and reload Caddy?" \
|
||
|| { gum style --foreground "$GREY_LIGHT" " Cancelled."; press_enter; return; }
|
||
|
||
ssh_run "rm -f ${SITES_DIR}/${CHOICE}"
|
||
|
||
echo ""
|
||
gum style --foreground "green" " ✔ Deleted ${SITES_DIR}/${CHOICE}"
|
||
|
||
reload_caddy && gum style --foreground "green" " ✔ Caddy reloaded successfully." \
|
||
|| gum style --foreground "red" " ✗ Caddy reload failed – check logs."
|
||
press_enter
|
||
}
|
||
|
||
# ── Main menu loop ────────────────────────────────────────────
|
||
|
||
while true; do
|
||
draw_title
|
||
|
||
MENU_CHOICE=$(gum choose \
|
||
--cursor "▶ " \
|
||
--cursor.foreground "$ORANGE" \
|
||
--selected.foreground "$ORANGE" \
|
||
--header " What would you like to do?" \
|
||
--header.foreground "$GREY_LIGHT" \
|
||
--height 6 \
|
||
" 1 Add a new site config" \
|
||
" 2 List / view site configs" \
|
||
" 3 Delete a site config" \
|
||
" ✕ Exit")
|
||
|
||
case "$MENU_CHOICE" in
|
||
*"1"*) add_config ;;
|
||
*"2"*) list_configs ;;
|
||
*"3"*) delete_config ;;
|
||
*"✕"*|"") clear; exit 0 ;;
|
||
esac
|
||
done
|