added support for multiple routes multiple ports

This commit is contained in:
Jakub Dorfman 2026-05-20 00:21:25 +02:00
parent 7399cf83f9
commit 7fd2531501
4 changed files with 445 additions and 151 deletions

View file

@ -15,12 +15,38 @@ FORWARD_AUTH_BLOCK=' forward_auth 127.0.0.1:9091 {
}
'
validate_route_entry() {
local entry="$1"
# Format: domain|upstream (upstream = host:port)
local domain="${entry%%|*}"
local upstream="${entry#*|}"
[[ -n "${domain}" ]] || fail "empty domain in route entry '${entry}'"
[[ -n "${upstream}" ]] || fail "empty upstream in route entry '${entry}'"
[[ "${entry}" == *"|"* ]] || fail "route entry '${entry}' missing '|' separator (expected domain|upstream)"
validate_single_domain "${domain}"
# Validate upstream has a port
local upstream_port="${upstream##*:}"
[[ "${upstream_port}" =~ ^[0-9]+$ ]] || fail "upstream '${upstream}' missing numeric port in route entry '${entry}'"
validate_port "${upstream_port}"
}
validate_routes() {
local routes_str="$1"
IFS=',' read -ra entries <<< "${routes_str}"
[[ ${#entries[@]} -ge 1 ]] || fail "at least one route is required"
for entry in "${entries[@]}"; do
entry="$(echo "${entry}" | xargs)"
validate_route_entry "${entry}"
done
}
usage() {
cat <<'EOF'
panelctl - minimal app panel helper
Usage:
panelctl init <name> <domains> <port> [auth]
panelctl init <name> "<domain>|<upstream>[,...]" [auth]
panelctl set-routes <name> "<domain>|<upstream>[,...]"
panelctl render-route <name>
panelctl deploy <name>
panelctl restart <name>
@ -36,14 +62,15 @@ Usage:
panelctl list
panelctl show <name>
Domains can be comma-separated for multiple domains:
panelctl init myapp "app.example.com,www.example.com" 18080 true
Each route is a domain|upstream pair. Upstream is host:port.
Multiple routes are comma-separated:
panelctl init myapp "app.example.com|127.0.0.1:18080,api.example.com|127.0.0.1:18081" true
Wildcard domains are supported (requires DNS challenge in Caddy):
panelctl init myapp "*.example.com" 18080 true
panelctl init myapp "*.example.com|127.0.0.1:18080" true
Examples:
panelctl init whoami whoami.srazka.com 18080 true
panelctl init whoami "whoami.srazka.com|127.0.0.1:18080" true
panelctl deploy whoami
panelctl restart whoami
panelctl status whoami
@ -128,6 +155,23 @@ load_app() {
[[ -f "${manifest}" ]] || fail "app '${name}' does not exist"
# shellcheck disable=SC1090
source "${manifest}"
# Backward compat: migrate old APP_DOMAIN/APP_PORT/APP_UPSTREAM to APP_ROUTES
if [[ -z "${APP_ROUTES:-}" && -n "${APP_DOMAIN:-}" ]]; then
local upstream="${APP_UPSTREAM:-127.0.0.1:${APP_PORT:-18080}}"
local routes=""
local domains_str="${APP_DOMAINS:-${APP_DOMAIN}}"
IFS=',' read -ra domain_arr <<< "${domains_str}"
for d in "${domain_arr[@]}"; do
d="$(echo "${d}" | xargs)"
if [[ -n "${routes}" ]]; then
routes="${routes},${d}|${upstream}"
else
routes="${d}|${upstream}"
fi
done
APP_ROUTES="${routes}"
fi
}
compose_command() {
@ -203,19 +247,24 @@ run_compose() {
write_default_compose() {
local name="$1"
local port="$2"
local routes="$2"
local stack_dir
local volume_dir
stack_dir="$(app_stack_dir "${name}")"
volume_dir="$(app_volume_dir "${name}")"
# Use first route's upstream port for the default compose mapping
local first_route="${routes%%,*}"
local first_upstream="${first_route#*|}"
local container_port="${first_upstream##*:}"
cat >"${stack_dir}/compose.yaml" <<EOF
services:
app:
image: docker.io/traefik/whoami:latest
restart: unless-stopped
ports:
- "127.0.0.1:${port}:80"
- "127.0.0.1:${container_port}:80"
volumes:
- ${volume_dir}/data:/data
EOF
@ -223,9 +272,8 @@ EOF
write_manifest() {
local name="$1"
local domains="$2"
local port="$3"
local auth="$4"
local routes="$2"
local auth="$3"
local manifest
local stack_dir
local volume_dir
@ -236,17 +284,9 @@ write_manifest() {
volume_dir="$(app_volume_dir "${name}")"
route_file="$(app_route_file)"
# First domain is the primary (used for APP_DOMAIN backward compat)
local primary_domain
IFS=',' read -ra domain_arr <<< "${domains}"
primary_domain="$(echo "${domain_arr[0]}" | xargs)"
cat >"${manifest}" <<EOF
APP_NAME="${name}"
APP_DOMAIN="${primary_domain}"
APP_DOMAINS="${domains}"
APP_PORT="${port}"
APP_UPSTREAM="127.0.0.1:${port}"
APP_ROUTES="${routes}"
APP_AUTH_PROTECTED="${auth}"
APP_STACK_DIR="${stack_dir}"
APP_COMPOSE_FILE="${stack_dir}/compose.yaml"
@ -257,13 +297,11 @@ EOF
cmd_init() {
local name="$1"
local domains="$2"
local port="$3"
local auth="${4:-true}"
local routes="$2"
local auth="${3:-true}"
validate_name "${name}"
validate_domains "${domains}"
validate_port "${port}"
validate_routes "${routes}"
[[ "${auth}" == "true" || "${auth}" == "false" ]] || fail "auth must be true or false"
ensure_base_dirs
@ -278,8 +316,8 @@ cmd_init() {
[[ ! -f "${manifest}" ]] || fail "app '${name}' already exists"
mkdir -p "${stack_dir}" "${volume_dir}/data"
write_default_compose "${name}" "${port}"
write_manifest "${name}" "${domains}" "${port}" "${auth}"
write_default_compose "${name}" "${routes}"
write_manifest "${name}" "${routes}" "${auth}"
cmd_render_route "${name}"
log info "initialized app '${name}'"
@ -295,19 +333,6 @@ cmd_render_route() {
auth_block="${FORWARD_AUTH_BLOCK}"
fi
# Build domain list for Caddy block header.
local caddy_domains=""
local domains_str="${APP_DOMAINS:-${APP_DOMAIN}}"
IFS=',' read -ra domain_arr <<< "${domains_str}"
for d in "${domain_arr[@]}"; do
d="$(echo "${d}" | xargs)"
if [[ -n "${caddy_domains}" ]]; then
caddy_domains="${caddy_domains}, ${d}"
else
caddy_domains="${d}"
fi
done
local route_file
route_file="$(app_route_file)"
@ -323,12 +348,17 @@ cmd_render_route() {
{
printf "# route:%s:start\n" "${name}"
printf "%s {\n" "${caddy_domains}"
if [[ -n "${auth_block}" ]]; then
printf "%s\n" "${auth_block}"
fi
printf " reverse_proxy %s\n" "${APP_UPSTREAM}"
printf "}\n"
IFS=',' read -ra route_entries <<< "${APP_ROUTES}"
for entry in "${route_entries[@]}"; do
entry="$(echo "${entry}" | xargs)"
local domain="${entry%%|*}"
local upstream="${entry#*|}"
if [[ -n "${auth_block}" ]]; then
printf "%s {\n%s reverse_proxy %s\n}\n" "${domain}" "${auth_block}" "${upstream}"
else
printf "%s {\n reverse_proxy %s\n}\n" "${domain}" "${upstream}"
fi
done
printf "# route:%s:end\n" "${name}"
} >>"${tmp}"
@ -452,6 +482,45 @@ cmd_remove() {
log info "removed app '${name}'"
}
cmd_set_routes() {
local name="$1"
local routes="$2"
local manifest
validate_name "${name}"
validate_routes "${routes}"
manifest="$(app_manifest "${name}")"
[[ -f "${manifest}" ]] || fail "app '${name}' does not exist"
# Update APP_ROUTES in the manifest file, strip old fields, preserve others
local tmp
tmp="$(mktemp)"
local found_routes=false
while IFS= read -r line; do
case "${line}" in
APP_ROUTES=*)
printf 'APP_ROUTES="%s"\n' "${routes}" >> "${tmp}"
found_routes=true
;;
APP_DOMAIN=*|APP_DOMAINS=*|APP_PORT=*|APP_UPSTREAM=*)
# Strip old format fields
;;
*)
printf '%s\n' "${line}" >> "${tmp}"
;;
esac
done < "${manifest}"
if ! "${found_routes}"; then
printf 'APP_ROUTES="%s"\n' "${routes}" >> "${tmp}"
fi
install -m 0664 "${tmp}" "${manifest}"
# Re-render Caddy routes (auto-reloads via systemd.path watcher)
cmd_render_route "${name}"
log info "updated routes for app '${name}'. Edit compose file if new ports need exposing."
}
cmd_backup() {
local name="$1"
validate_name "${name}"
@ -602,9 +671,29 @@ cmd_list() {
source /dev/null # reset any leftover variables
unset APP_REPO_URL APP_REPO_BRANCH APP_REPO_DIR 2>/dev/null || true
source "${mf}"
local domains="${APP_DOMAINS:-${APP_DOMAIN}}"
# Backward compat: build APP_ROUTES from old format
local routes="${APP_ROUTES:-}"
if [[ -z "${routes}" && -n "${APP_DOMAIN:-}" ]]; then
local upstream="${APP_UPSTREAM:-127.0.0.1:${APP_PORT:-18080}}"
local domains_str="${APP_DOMAINS:-${APP_DOMAIN}}"
IFS=',' read -ra domain_arr <<< "${domains_str}"
for d in "${domain_arr[@]}"; do
d="$(echo "${d}" | xargs)"
if [[ -n "${routes}" ]]; then
routes="${routes},${d}|${upstream}"
else
routes="${d}|${upstream}"
fi
done
fi
# Show abbreviated: first route's domain + upstream, and count
local first_route="${routes%%,*}"
local route_count=1
if [[ "${routes}" == *","* ]]; then
route_count="$(( $(grep -o ',' <<< "${routes}" | wc -l) + 1 ))"
fi
local repo_info="${APP_REPO_URL:-}"
echo "${APP_NAME} ${domains} ${APP_UPSTREAM} auth=${APP_AUTH_PROTECTED} ${repo_info}"
echo "${APP_NAME} ${first_route} routes=${route_count} auth=${APP_AUTH_PROTECTED} ${repo_info}"
done
if [[ "${found}" -eq 0 ]]; then
@ -626,8 +715,12 @@ main() {
case "${cmd}" in
init)
[[ $# -ge 4 ]] || fail "usage: panelctl init <name> <domains> <port> [auth]"
cmd_init "$2" "$3" "$4" "${5:-true}"
[[ $# -ge 3 ]] || fail "usage: panelctl init <name> <routes> [auth]"
cmd_init "$2" "$3" "${4:-true}"
;;
set-routes)
[[ $# -eq 3 ]] || fail "usage: panelctl set-routes <name> <routes>"
cmd_set_routes "$2" "$3"
;;
render-route)
[[ $# -eq 2 ]] || fail "usage: panelctl render-route <name>"