mirror of
https://github.com/dredx/prole.git
synced 2026-09-24 18: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