mirror of
https://github.com/dredx/prole.git
synced 2026-09-23 10:13:58 +00:00
- Refactored installer UI with updated canvas rendering, sidebar navigation, and footer buttons. - Enhanced styling for macOS compatibility and consistent design across controls. - Added Pytest-based unit tests for `screen.py` and `config.py`. - Expanded dependency catalog with new tools like `tshark` and `pyshark`. - Improved error tolerance for background rendering and added placeholders for Kerberos configuration.
538 lines
15 KiB
Bash
Executable File
538 lines
15 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
set -u
|
|
|
|
# init_port_forwards.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="${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> [component]
|
|
|
|
Options:
|
|
-c, --config-file=FILE Path to local-ports.properties (XML)
|
|
-v, --verbose Verbose output
|
|
|
|
Examples:
|
|
$PROG -c ./port-mappings.properties start
|
|
$PROG stop openbao
|
|
$PROG --verbose status
|
|
EOF
|
|
}
|
|
|
|
TARGET_ID=""
|
|
|
|
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
|
|
}
|
|
|
|
# ---- Preflight: Docker + k3d awareness ----
|
|
# Return 0 when Docker CLI can talk to a running daemon.
|
|
docker_is_running() {
|
|
if ! have docker; then
|
|
return 1
|
|
fi
|
|
docker info >/dev/null 2>&1
|
|
}
|
|
|
|
# Hard-fail with clean guidance when Docker is not up (used for start/restart).
|
|
ensure_docker_running() {
|
|
if docker_is_running; then
|
|
return 0
|
|
fi
|
|
if ! have docker; then
|
|
err "Docker CLI not found. Please install Docker Desktop or docker CLI."
|
|
else
|
|
err "Docker is not running. Start Docker Desktop and wait until it is ready."
|
|
fi
|
|
err "Tip (macOS): open -a Docker"
|
|
err "Then re-run: $PROG start"
|
|
exit 3
|
|
}
|
|
|
|
# Detect k3d cluster name from env or current kubectl context.
|
|
# - If K3D_CLUSTER is set, use it.
|
|
# - Else, if current-context starts with 'k3d-', strip prefix to get name.
|
|
detect_k3d_context() {
|
|
if [ -n "${K3D_CLUSTER:-}" ]; then
|
|
printf '%s' "$K3D_CLUSTER"
|
|
return 0
|
|
fi
|
|
local ctx
|
|
ctx="$($KUBECTL config current-context 2>/dev/null || echo)"
|
|
case "$ctx" in
|
|
k3d-*) printf '%s' "${ctx#k3d-}"; return 0 ;;
|
|
*) return 1 ;;
|
|
esac
|
|
}
|
|
|
|
# Return 0 if the given k3d cluster exists and is running.
|
|
k3d_cluster_is_running() {
|
|
local name="$1"
|
|
have k3d || return 1
|
|
# Use json output when available; fall back to grep otherwise
|
|
if k3d cluster list -o json >/dev/null 2>&1; then
|
|
k3d cluster list -o json 2>/dev/null | grep -q '"name"\s*:\s*"'"$name"'"' && \
|
|
k3d cluster list -o json 2>/dev/null | sed -n 's/.*"name"\s*:\s*"\([^"]\+\)".*"serversRunning"\s*:\s*\([0-9]\+\).*/\1 \2/p' | awk -v n="$name" '$1==n {exit ($2>0)?0:1}'
|
|
return $?
|
|
else
|
|
k3d cluster list 2>/dev/null | grep -E "^$name\s" | grep -q running
|
|
return $?
|
|
fi
|
|
}
|
|
|
|
# If current context indicates k3d, ensure the cluster is up; otherwise no-op.
|
|
ensure_k3d_ready_if_applicable() {
|
|
local k3d_name
|
|
if ! k3d_name="$(detect_k3d_context)"; then
|
|
return 0
|
|
fi
|
|
if ! have k3d; then
|
|
err "k3d is not installed but kubectl context suggests k3d (context=$("$KUBECTL" config current-context))."
|
|
err "Install k3d: brew install k3d (macOS)"
|
|
err "Or switch context: kubectl config use-context <non-k3d-context>"
|
|
exit 4
|
|
fi
|
|
if ! k3d_cluster_is_running "$k3d_name"; then
|
|
err "k3d cluster '$k3d_name' is not running."
|
|
err "Start it: k3d cluster start $k3d_name"
|
|
err "Then re-run: $PROG start"
|
|
exit 4
|
|
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
|
|
|
|
# Filter by TARGET_ID if set
|
|
if [ -n "$TARGET_ID" ] && [ "$id" != "$TARGET_ID" ]; then
|
|
in_mapping=0
|
|
buffer=""
|
|
continue
|
|
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() {
|
|
local id="$1" ns="$2" target="$3" address="$4" hostPort="$5" servicePort="$6" protocol="$7" description="$8"
|
|
local pidfile logfile cmd pid
|
|
|
|
# Aggressively stop existing processes before starting to avoid port conflicts
|
|
stop_port_forward "$id" "$ns" "$target" "$address" "$hostPort" "$servicePort" "$protocol" "$description"
|
|
|
|
pidfile="$(pid_file_for "$id")"
|
|
logfile="$(log_file_for "$id")"
|
|
cmd="$(build_port_forward_cmd "$ns" "$target" "$address" "$hostPort" "$servicePort")"
|
|
|
|
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")"
|
|
pid=""
|
|
if [ -f "$pidfile" ]; then
|
|
pid="$(read_pid "$pidfile" || true)"
|
|
fi
|
|
|
|
if [ -n "${pid:-}" ] && 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
|
|
if [ -f "$pidfile" ]; then
|
|
vlog "$id stale pidfile (pid=$pid not running)"
|
|
fi
|
|
fi
|
|
remove_pidfile "$pidfile"
|
|
|
|
# Aggressively remove any other matching kubectl port-forward processes
|
|
# Search for processes that match: kubectl port-forward -n <ns> ... <target> <hostPort>:<servicePort>
|
|
local extra_pids
|
|
extra_pids=$(ps -ef | grep "port-forward" | grep "\-n" | grep "$ns" | grep "$target" | grep "$hostPort:$servicePort" | grep -v grep | awk '{print $2}')
|
|
|
|
for epid in $extra_pids; do
|
|
if [ "$epid" != "$pid" ]; then
|
|
log "Cleaning up orphan process for $id (pid=$epid)"
|
|
kill -9 "$epid" >/dev/null 2>&1 || true
|
|
fi
|
|
done
|
|
}
|
|
|
|
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
|
|
# Preflight: require Docker daemon and k3d (if applicable)
|
|
ensure_docker_running
|
|
ensure_k3d_ready_if_applicable
|
|
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
|
|
# Preflight: require Docker daemon and k3d (if applicable)
|
|
ensure_docker_running
|
|
ensure_k3d_ready_if_applicable
|
|
foreach_mapping stop_port_forward
|
|
foreach_mapping start_port_forward
|
|
}
|
|
|
|
do_status() {
|
|
validate_env
|
|
# Non-fatal awareness messages
|
|
if docker_is_running; then
|
|
log "[OK] Docker daemon is running"
|
|
else
|
|
if have docker; then
|
|
log "[WARN] Docker is not running"
|
|
else
|
|
log "[WARN] Docker CLI not found"
|
|
fi
|
|
fi
|
|
|
|
local _k3d_name
|
|
if _k3d_name="$(detect_k3d_context)"; then
|
|
if have k3d; then
|
|
if k3d_cluster_is_running "$_k3d_name"; then
|
|
log "[OK] k3d cluster '$_k3d_name' is running"
|
|
else
|
|
log "[WARN] k3d cluster '$_k3d_name' is not running"
|
|
fi
|
|
else
|
|
log "[WARN] k3d not installed but context suggests k3d (cluster='$_k3d_name')"
|
|
fi
|
|
fi
|
|
|
|
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)
|
|
if [ -z "$ACTION" ]; then
|
|
ACTION="$1"
|
|
else
|
|
TARGET_ID="$1"
|
|
fi
|
|
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
|
|
;;
|
|
*)
|
|
if [ -z "$ACTION" ]; then
|
|
err "Unknown arg: $1"
|
|
usage
|
|
exit 2
|
|
fi
|
|
TARGET_ID="$1"
|
|
shift
|
|
;;
|
|
esac
|
|
done
|
|
|
|
[ -n "$ACTION" ] || { usage; exit 2; }
|
|
|
|
case "$ACTION" in
|
|
start) do_start ;;
|
|
stop) do_stop ;;
|
|
restart) do_restart ;;
|
|
status) do_status ;;
|
|
esac
|