mirror of
https://github.com/dredx/prole.git
synced 2026-09-24 17:44:33 +00:00
Merge branch 'main' of github.com:dredx/prole
This commit is contained in:
commit
9fbb728b1b
417
etc/init-port-fowards.sh
Executable file
417
etc/init-port-fowards.sh
Executable file
@ -0,0 +1,417 @@
|
||||
#!/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
|
||||
Loading…
Reference in New Issue
Block a user