Files
Starship/scripts/caddy.sh
T
2026-05-29 11:17:00 +01:00

373 lines
12 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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" \
"No" "Yes")
# 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