prole/etc/init-port-fowards.sh

418 lines
11 KiB
Bash
Executable File

#!/usr/bin/env bash
set -u
# init-kpf.sh
# Portable-ish (macOS, Ubuntu, Raspberry Pi OS, Alpine) bash init-style script
# Manages kubectl port-forward daemons defined in an XML file.
PROG="init-port-forwards"
PROLE_HOME="/Users/chrisfu/dev/prole"
VERBOSE=0
CONFIG_FILE="$PROLE_HOME/conf/port-mappings.properties"
usage() {
cat <<EOF
Usage:
$PROG [-v|--verbose] [-c|--config-file=FILE] <start|stop|restart|status>
Options:
-c, --config-file=FILE Path to local-ports.properties (XML)
-v, --verbose Verbose output
Examples:
$PROG -c ./port-mappings.properties start
$PROG --verbose status
EOF
}
log() { printf '%s\n' "$*"; }
vlog() { [ "$VERBOSE" -eq 1 ] && printf '[verbose] %s\n' "$*" || true; }
err() { printf '[error] %s\n' "$*" >&2; }
have() { command -v "$1" >/dev/null 2>&1; }
# Choose a writable state dir for pid/log files:
# - Prefer XDG_RUNTIME_DIR if set and writable
# - Else /var/run if writable (rare without root)
# - Else ~/.local/state
# - Else /tmp
state_dir() {
if [ -n "${XDG_RUNTIME_DIR:-}" ] && [ -w "${XDG_RUNTIME_DIR:-}" ]; then
printf '%s/%s' "$XDG_RUNTIME_DIR" "$PROG"
return
fi
if [ -d "/var/run" ] && [ -w "/var/run" ]; then
printf '/var/run/%s' "$PROG"
return
fi
if [ -n "${HOME:-}" ]; then
mkdir -p "$HOME/.local/state" >/dev/null 2>&1 || true
if [ -d "$HOME/.local/state" ] && [ -w "$HOME/.local/state" ]; then
printf '%s/.local/state/%s' "$HOME" "$PROG"
return
fi
fi
printf '/tmp/%s' "$PROG"
}
STATE_DIR="$(state_dir)"
PID_DIR="$STATE_DIR/pids"
LOG_DIR="$STATE_DIR/logs"
ensure_dirs() {
mkdir -p "$PID_DIR" "$LOG_DIR" 2>/dev/null || true
if [ ! -d "$PID_DIR" ] || [ ! -d "$LOG_DIR" ]; then
err "Unable to create state directories under: $STATE_DIR"
exit 1
fi
}
# Use kubectl for actions; kubecolor is used only for prettier status output.
KUBECTL="kubectl"
detect_kubectl() {
if have kubectl; then
KUBECTL="kubectl"
else
err "kubectl not found in PATH"
exit 1
fi
}
# Basic environment validation
validate_env() {
detect_kubectl
ensure_dirs
if ! "$KUBECTL" version --client >/dev/null 2>&1; then
err "kubectl seems broken or not executable"
exit 1
fi
# Can we reach the cluster?
if ! "$KUBECTL" cluster-info >/dev/null 2>&1; then
err "kubectl cannot reach a cluster (check KUBECONFIG/context)"
err "Try: kubectl config get-contexts && kubectl config use-context <ctx>"
exit 1
fi
}
pid_file_for() { printf '%s/%s.pid' "$PID_DIR" "$1"; }
log_file_for() { printf '%s/%s.log' "$LOG_DIR" "$1"; }
is_pid_running() {
# Return 0 if pid exists and running, else 1
# kill -0 is portable.
local pid="$1"
[ -n "$pid" ] && kill -0 "$pid" >/dev/null 2>&1
}
read_pid() {
local pf="$1"
[ -f "$pf" ] || return 1
# shellcheck disable=SC2162
read pid <"$pf" || return 1
printf '%s' "$pid"
}
write_pid() {
local pf="$1" pid="$2"
printf '%s\n' "$pid" >"$pf"
}
remove_pidfile() {
local pf="$1"
rm -f "$pf" >/dev/null 2>&1 || true
}
# ---- XML parsing (simple attribute extraction) ----
# We parse lines containing: <mapping ... />
# Attributes must use double quotes in the XML (as in the sample).
get_attr() {
# $1 = line, $2 = attribute name
# outputs value or empty
printf '%s\n' "$1" | sed -n "s/.*$2=\"\([^\"]*\)\".*/\1/p"
}
foreach_mapping() {
# Calls a provided function with mapping fields:
# callback id ns target address hostPort servicePort protocol description
local callback="$1"
[ -f "$CONFIG_FILE" ] || { err "Config file not found: $CONFIG_FILE"; exit 1; }
# Support both single-line and multi-line self-closing mapping tags, e.g.:
# <mapping id="x" ... /> OR lines spanning multiple lines until "/>".
local in_mapping=0 in_comment=0 buffer="" line
while IFS= read -r line; do
# Handle XML comments: skip anything between <!-- and -->
if [ $in_comment -eq 1 ]; then
case "$line" in
*"-->"*) in_comment=0; continue ;;
*) continue ;;
esac
fi
case "$line" in
*"<!--"*)
case "$line" in
*"-->"*)
# single-line comment; skip line
continue
;;
*)
in_comment=1
continue
;;
esac
;;
esac
if [ $in_mapping -eq 0 ]; then
case "$line" in
*"<mapping"*)
in_mapping=1
buffer="$line"
;;
*)
continue
;;
esac
else
# Accumulate lines until we see the closing '/>'
buffer="$buffer $line"
fi
if [ $in_mapping -eq 1 ] && printf '%s' "$line" | grep -q "/>"; then
# Normalize whitespace to make attribute extraction robust
local merged
merged=$(printf '%s\n' "$buffer" | tr '\n' ' ' | sed 's/[[:space:]]\+/ /g')
local id ns target address hostPort servicePort protocol description
id="$(get_attr "$merged" "id")"
ns="$(get_attr "$merged" "namespace")"
target="$(get_attr "$merged" "target")"
address="$(get_attr "$merged" "address")"
hostPort="$(get_attr "$merged" "hostPort")"
servicePort="$(get_attr "$merged" "servicePort")"
protocol="$(get_attr "$merged" "protocol")"
description="$(get_attr "$merged" "description")"
# Basic validation
if [ -z "$id" ] || [ -z "$ns" ] || [ -z "$target" ] || [ -z "$hostPort" ] || [ -z "$servicePort" ]; then
err "Invalid mapping (missing required attributes): $merged"
exit 1
fi
if [ -z "$address" ]; then address="127.0.0.1"; fi
if [ -z "$protocol" ]; then protocol="TCP"; fi
vlog "mapping: id=$id ns=$ns target=$target address=$address hostPort=$hostPort servicePort=$servicePort protocol=$protocol"
"$callback" "$id" "$ns" "$target" "$address" "$hostPort" "$servicePort" "$protocol" "$description"
# Reset accumulator
in_mapping=0
buffer=""
fi
done <"$CONFIG_FILE"
}
build_port_forward_cmd() {
# echo a command string
local ns="$1" target="$2" address="$3" hostPort="$4" servicePort="$5"
# kubectl port-forward -n <ns> --address <addr> <target> <local>:<remote>
printf '%s port-forward -n %s --address %s %s %s:%s' \
"$KUBECTL" "$ns" "$address" "$target" "$hostPort" "$servicePort"
}
start_port_forward() {
echo "start_port_forward()"
local id="$1" ns="$2" target="$3" address="$4" hostPort="$5" servicePort="$6" protocol="$7" description="$8"
local pf pidfile logfile cmd pid
pidfile="$(pid_file_for "$id")"
logfile="$(log_file_for "$id")"
cmd="$(build_port_forward_cmd "$ns" "$target" "$address" "$hostPort" "$servicePort")"
echo $cmd
# If pidfile exists and process is alive, skip
if [ -f "$pidfile" ]; then
pid="$(read_pid "$pidfile" || true)"
if [ -n "${pid:-}" ] && is_pid_running "$pid"; then
vlog "$id already running (pid=$pid)"
return 0
else
# stale pidfile
remove_pidfile "$pidfile"
fi
fi
log "Starting: $id $address:$hostPort -> $target:$servicePort ($ns) ${description:-}"
vlog "Command: $cmd"
# Start in background, keep output in log.
# nohup is available on macOS/Linux; redirect stdin from /dev/null to detach.
nohup sh -c "$cmd" >>"$logfile" 2>&1 </dev/null &
pid="$!"
write_pid "$pidfile" "$pid"
# Quick verification
sleep 0.2
if is_pid_running "$pid"; then
vlog "$id started (pid=$pid, log=$logfile)"
return 0
else
err "$id failed to start (see log: $logfile)"
remove_pidfile "$pidfile"
return 1
fi
}
stop_port_forward() {
local id="$1" ns="$2" target="$3" address="$4" hostPort="$5" servicePort="$6" protocol="$7" description="$8"
local pidfile pid
pidfile="$(pid_file_for "$id")"
if [ ! -f "$pidfile" ]; then
vlog "$id not running (no pidfile)"
return 0
fi
pid="$(read_pid "$pidfile" || true)"
if [ -z "${pid:-}" ]; then
remove_pidfile "$pidfile"
return 0
fi
if is_pid_running "$pid"; then
log "Stopping: $id (pid=$pid)"
kill "$pid" >/dev/null 2>&1 || true
# wait a moment, then SIGKILL if needed
local i
for i in 1 2 3 4 5; do
if ! is_pid_running "$pid"; then break; fi
sleep 0.2
done
if is_pid_running "$pid"; then
err "$id did not stop gracefully; sending SIGKILL"
kill -9 "$pid" >/dev/null 2>&1 || true
fi
else
vlog "$id stale pidfile (pid=$pid not running)"
fi
remove_pidfile "$pidfile"
}
status_one() {
local id="$1" ns="$2" target="$3" address="$4" hostPort="$5" servicePort="$6" protocol="$7" description="$8"
local pidfile pid
pidfile="$(pid_file_for "$id")"
pid=""
if [ -f "$pidfile" ]; then
pid="$(read_pid "$pidfile" || true)"
fi
if [ -n "${pid:-}" ] && is_pid_running "$pid"; then
printf 'RUNNING %-12s pid=%-7s %s:%s -> %s:%s ns=%s %s\n' \
"$id" "$pid" "$address" "$hostPort" "$target" "$servicePort" "$ns" "${description:-}"
# ps details (portable flags vary; use a conservative format)
ps -p "$pid" -o pid=,ppid=,etime=,command= 2>/dev/null | sed 's/^/ /' || true
else
printf 'STOPPED %-12s (no live pid) %s:%s -> %s:%s ns=%s %s\n' \
"$id" "$address" "$hostPort" "$target" "$servicePort" "$ns" "${description:-}"
fi
}
do_start() {
validate_env
foreach_mapping start_port_forward
}
do_stop() {
# stop doesn't require cluster access, but it does need state dirs
ensure_dirs
foreach_mapping stop_port_forward
}
do_restart() {
validate_env
foreach_mapping stop_port_forward
foreach_mapping start_port_forward
}
do_status() {
validate_env
log "== Context / Cluster =="
"$KUBECTL" config current-context 2>/dev/null | sed 's/^/ context: /' || true
"$KUBECTL" cluster-info 2>/dev/null | sed 's/^/ /' || true
log ""
log "== Port-forward processes =="
foreach_mapping status_one
log ""
log "== Quick k3d awareness checks (best-effort) =="
# If user is on k3d, current-context often includes k3d-... but not guaranteed.
# Show nodes + a few namespaces/services related to mappings (best effort).
"$KUBECTL" get nodes -o wide 2>/dev/null | sed 's/^/ /' || true
log ""
"$KUBECTL" get ns 2>/dev/null | sed 's/^/ /' || true
log ""
# For each mapping, try to show target existence
log "== Target existence (best-effort) =="
_target_check() {
local id="$1" ns="$2" target="$3" address="$4" hostPort="$5" servicePort="$6" protocol="$7" description="$8"
# kubectl get <target> -n <ns>
# If target is like "svc/name", "deploy/name", etc.
if "$KUBECTL" get -n "$ns" "$target" >/dev/null 2>&1; then
printf 'OK %-12s %s (ns=%s)\n' "$id" "$target" "$ns"
else
printf 'MISSING %-12s %s (ns=%s)\n' "$id" "$target" "$ns"
fi
}
foreach_mapping _target_check
}
# ---- arg parsing ----
ACTION=""
while [ $# -gt 0 ]; do
case "$1" in
start|stop|restart|status)
ACTION="$1"
shift
;;
-v|--verbose)
VERBOSE=1
shift
;;
-c)
shift
[ $# -gt 0 ] || { err "-c requires a file path"; usage; exit 2; }
CONFIG_FILE="$1"
shift
;;
--config-file=*)
CONFIG_FILE="${1#*=}"
shift
;;
-h|--help)
usage
exit 0
;;
*)
err "Unknown arg: $1"
usage
exit 2
;;
esac
done
[ -n "$ACTION" ] || { usage; exit 2; }
case "$ACTION" in
start) do_start ;;
stop) do_stop ;;
restart) do_restart ;;
status) do_status ;;
esac