#!/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" SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) # shellcheck disable=SC1090 source "$SCRIPT_DIR/prole_cfg.sh" PROLE_HOME="${PROLE_HOME:-/Users/chrisfu/dev/prole}" VERBOSE=0 CONFIG_FILE="" CONFIG_FILE_SOURCE="auto" PORT_FORWARD_ENABLED="${PORT_FORWARD_ENABLED:-}" PORT_FORWARD_K3D_ENABLED="${PORT_FORWARD_K3D_ENABLED:-}" PORT_FORWARD_K3S_ENABLED="${PORT_FORWARD_K3S_ENABLED:-}" PORT_FORWARD_CONFIG_FILE="${PORT_FORWARD_CONFIG_FILE:-}" PORT_FORWARD_CONFIG_FILE_K3D="${PORT_FORWARD_CONFIG_FILE_K3D:-}" PORT_FORWARD_CONFIG_FILE_K3S="${PORT_FORWARD_CONFIG_FILE_K3S:-}" PORT_FORWARD_HOSTS="${PORT_FORWARD_HOSTS:-}" PORT_FORWARD_K3D_HOSTS="${PORT_FORWARD_K3D_HOSTS:-}" PORT_FORWARD_K3S_HOSTS="${PORT_FORWARD_K3S_HOSTS:-}" usage() { cat < [component] Options: -c, --config-file=FILE Path to an optional XML mappings file -v, --verbose Verbose output -f, --force Force: kill existing processes blocking ports Examples: $PROG -c ./port-mappings.xml start $PROG stop openbao $PROG --verbose status Config (prole.cfg): PORT_FORWARD_K3D_MAPPING_1 = id=dashboard;namespace=kubernetes-dashboard;target=svc/kubernetes-dashboard-kong-proxy;address=127.0.0.1;hostPort=8443;servicePort=443;protocol=TCP;description=Kubernetes Dashboard PORT_FORWARD_K3S_MAPPING_1 = id=opentofu;namespace=\${NAMESPACE};target=svc/opentofu;address=0.0.0.0;hostPort=8080;servicePort=8080;protocol=TCP;description=OpenTofu EOF } TARGET_ID="" FORCE=0 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; } # ---- Config helpers / derived defaults ---- cfg_file() { if [ -n "${PROLE_CONF:-}" ] && [ -f "$PROLE_CONF/prole.cfg" ]; then printf '%s' "$PROLE_CONF/prole.cfg" return 0 fi if [ -n "${PROLE_HOME:-}" ] && [ -f "$PROLE_HOME/conf/prole.cfg" ]; then printf '%s' "$PROLE_HOME/conf/prole.cfg" return 0 fi return 1 } CFG_FILE="$(cfg_file || true)" cfg_value() { local key="$1" [ -n "${CFG_FILE:-}" ] || return 0 awk -F= -v k="$key" ' /^[[:space:]]*;/ {next} /^[[:space:]]*#/ {next} /^[[:space:]]*\\[/ {next} { kk=$1 sub(/^[[:space:]]+/, "", kk) sub(/[[:space:]]+$/, "", kk) } kk == k { v=$2 sub(/^[[:space:]]+/, "", v) sub(/[[:space:]]+$/, "", v) val=v } END { print val } ' "$CFG_FILE" 2>/dev/null } is_truthy() { case "${1:-}" in 1|true|TRUE|True|yes|YES|Yes|on|ON|On) return 0 ;; esac return 1 } current_hostname() { local host="" if have hostname; then host="$(hostname -f 2>/dev/null || hostname 2>/dev/null || true)" fi if [ -z "$host" ] && [ -n "${HOSTNAME:-}" ]; then host="$HOSTNAME" fi printf '%s' "$host" } host_in_allowlist() { local list="$1" local host="$2" local short="${host%%.*}" list="${list//,/ }" for item in $list; do [ -z "$item" ] && continue if [ "$item" = "$host" ] || [ "$item" = "$short" ]; then return 0 fi done return 1 } port_forward_allowed_on_host() { local allow="" case "${PROLE_MODE:-}" in k3s) allow="${PORT_FORWARD_K3S_HOSTS:-${PORT_FORWARD_HOSTS:-}}" ;; k3d|"") allow="${PORT_FORWARD_K3D_HOSTS:-${PORT_FORWARD_HOSTS:-}}" ;; *) allow="${PORT_FORWARD_HOSTS:-}" ;; esac if [ -z "$allow" ]; then return 0 fi local host host="$(current_hostname)" if [ -z "$host" ]; then return 0 fi host_in_allowlist "$allow" "$host" } trim() { local s="$1" s="${s#"${s%%[![:space:]]*}"}" s="${s%"${s##*[![:space:]]}"}" printf '%s' "$s" } resolve_config_file() { if [ -n "${PORT_FORWARD_CONFIG_FILE:-}" ]; then printf '%s' "$PORT_FORWARD_CONFIG_FILE" return 0 fi case "${PROLE_MODE:-}" in k3s) if [ -n "${PORT_FORWARD_CONFIG_FILE_K3S:-}" ]; then printf '%s' "$PORT_FORWARD_CONFIG_FILE_K3S" return 0 fi ;; k3d|"") if [ -n "${PORT_FORWARD_CONFIG_FILE_K3D:-}" ]; then printf '%s' "$PORT_FORWARD_CONFIG_FILE_K3D" return 0 fi ;; esac printf '%s' "" } mapping_prefix_for_mode() { case "${PROLE_MODE:-}" in k3s) printf '%s' "PORT_FORWARD_K3S_MAPPING_" ;; k3d|"") printf '%s' "PORT_FORWARD_K3D_MAPPING_" ;; *) printf '%s' "PORT_FORWARD_MAPPING_" ;; esac } collect_cfg_mappings() { local prefix="$1" local entries=() key value while IFS='=' read -r key value; do case "$key" in ${prefix}[0-9]*) entries+=("$key=$value") ;; esac done < <(env) if [ "${#entries[@]}" -eq 0 ]; then return 1 fi printf '%s\n' "${entries[@]}" | awk -F= -v p="$prefix" '{ val=$0 sub("^[^=]+=", "", val) key=$1 sub(p, "", key) num=key+0 print num "\t" val }' | sort -n } parse_cfg_mapping() { local line="$1" local id="" ns="" target="" address="" hostPort="" servicePort="" protocol="" description="" local token key value IFS=';' read -ra parts <<<"$line" for token in "${parts[@]}"; do token="$(trim "$token")" [ -z "$token" ] && continue key="$(trim "${token%%=*}")" value="$(trim "${token#*=}")" case "$key" in id) id="$value" ;; namespace|ns) ns="$value" ;; target) target="$value" ;; address|addr) address="$value" ;; hostPort|host_port|local|host) hostPort="$value" ;; servicePort|service_port|remote) servicePort="$value" ;; protocol) protocol="$value" ;; description|desc) description="$value" ;; esac done if [ -n "${NAMESPACE:-}" ]; then ns="${ns//\$\{NAMESPACE\}/$NAMESPACE}" ns="${ns//\$\{PROLE_NAMESPACE\}/$NAMESPACE}" fi if [ -n "${PF_MONITORING_NAMESPACE:-}" ]; then ns="${ns//\$\{MONITORING_NAMESPACE\}/$PF_MONITORING_NAMESPACE}" ns="${ns//\$\{PROLE_MONITORING_NAMESPACE\}/$PF_MONITORING_NAMESPACE}" fi if [ -n "${PF_MANAGEMENT_NAMESPACE:-}" ]; then ns="${ns//\$\{MANAGEMENT_NAMESPACE\}/$PF_MANAGEMENT_NAMESPACE}" ns="${ns//\$\{PROLE_MANAGEMENT_NAMESPACE\}/$PF_MANAGEMENT_NAMESPACE}" fi if [ "$id" = "postgres" ]; then if [ -n "${DB_HOST_PORT:-}" ]; then hostPort="$DB_HOST_PORT" fi if [ "$SUPABASE_ENABLED_EFFECTIVE" -eq 1 ]; then ns="$SUPABASE_NAMESPACE" target="$SUPABASE_DB_TARGET" fi fi if [ -z "$address" ]; then address="127.0.0.1"; fi if [ -z "$protocol" ]; then protocol="TCP"; fi printf '%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n' \ "$id" "$ns" "$target" "$address" "$hostPort" "$servicePort" "$protocol" "$description" } port_forward_enabled_for_mode() { local val="" case "${PROLE_MODE:-}" in k3s) val="${PORT_FORWARD_K3S_ENABLED:-${PORT_FORWARD_ENABLED:-}}" ;; k3d|"") val="${PORT_FORWARD_K3D_ENABLED:-${PORT_FORWARD_ENABLED:-}}" ;; *) val="${PORT_FORWARD_ENABLED:-}" ;; esac if [ -z "$val" ]; then return 0 fi is_truthy "$val" } port_in_use() { local port="$1" if have lsof; then lsof -nP -iTCP:"$port" -sTCP:LISTEN >/dev/null 2>&1 && return 0 || return 1 fi if have ss; then ss -lnt 2>/dev/null | awk '{print $4}' | grep -E "[:.]$port$" >/dev/null 2>&1 && return 0 || return 1 fi if have netstat; then netstat -an 2>/dev/null | grep -E "[.:]$port[[:space:]]" | grep -qi listen && return 0 || return 1 fi return 1 } find_available_port() { local base="$1" local max="${2:-50}" local port="$base" local i=0 while [ "$i" -lt "$max" ]; do if ! port_in_use "$port"; then printf '%s' "$port" return 0 fi port=$((port + 1)) i=$((i + 1)) done printf '%s' "$base" } CFG_NAMESPACE="$(cfg_value "NAMESPACE")" CFG_MONITORING_NAMESPACE="$(cfg_value "MONITORING_NAMESPACE")" CFG_MANAGEMENT_NAMESPACE="$(cfg_value "MANAGEMENT_NAMESPACE")" CFG_PROLE_MONITORING_NAMESPACE="$(cfg_value "PROLE_MONITORING_NAMESPACE")" CFG_PROLE_MANAGEMENT_NAMESPACE="$(cfg_value "PROLE_MANAGEMENT_NAMESPACE")" PF_MONITORING_NAMESPACE="${MONITORING_NAMESPACE:-${PROLE_MONITORING_NAMESPACE:-${CFG_MONITORING_NAMESPACE:-${CFG_PROLE_MONITORING_NAMESPACE:-${CFG_NAMESPACE:-}}}}}" PF_MANAGEMENT_NAMESPACE="${MANAGEMENT_NAMESPACE:-${PROLE_MANAGEMENT_NAMESPACE:-${CFG_MANAGEMENT_NAMESPACE:-${CFG_PROLE_MANAGEMENT_NAMESPACE:-${CFG_NAMESPACE:-}}}}}" if [ -z "${PF_MONITORING_NAMESPACE:-}" ] && [ -n "${NAMESPACE:-}" ]; then PF_MONITORING_NAMESPACE="$NAMESPACE" fi if [ -z "${PF_MANAGEMENT_NAMESPACE:-}" ] && [ -n "${NAMESPACE:-}" ]; then PF_MANAGEMENT_NAMESPACE="$NAMESPACE" fi SUPABASE_NAMESPACE="${SUPABASE_NAMESPACE:-supabase}" SUPABASE_DB_SERVICE="${SUPABASE_DB_SERVICE:-db}" SUPABASE_DB_TARGET="${SUPABASE_DB_TARGET:-svc/$SUPABASE_DB_SERVICE}" SUPABASE_ENABLED_EFFECTIVE=0 if is_truthy "${SUPABASE_ENABLED:-}"; then SUPABASE_ENABLED_EFFECTIVE=1 elif is_truthy "${init_cluster_supabase_enabled:-}"; then SUPABASE_ENABLED_EFFECTIVE=1 fi PROLE_DB_ALT_PORT_BASE="${PROLE_DB_ALT_PORT_BASE:-15432}" PROLE_DB_ALT_PORT_EFFECTIVE="" if [ "$SUPABASE_ENABLED_EFFECTIVE" -eq 1 ]; then if [ -n "${PROLE_DB_ALT_PORT:-}" ]; then PROLE_DB_ALT_PORT_EFFECTIVE="$PROLE_DB_ALT_PORT" else PROLE_DB_ALT_PORT_EFFECTIVE="$(find_available_port "$PROLE_DB_ALT_PORT_BASE")" fi fi # 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 " exit 1 fi } ensure_supabase_ports() { [ "$SUPABASE_ENABLED_EFFECTIVE" -eq 1 ] || return 0 local wire_script="$SCRIPT_DIR/init_supabase_ports.sh" if [ ! -x "$wire_script" ]; then warn "Supabase port wiring script not found/executable: $wire_script" return 0 fi log "Ensuring Supabase DB wiring (db -> prole-db-rw, supabase-postgres -> 15432) ..." SUPABASE_NAMESPACE="$SUPABASE_NAMESPACE" \ "$wire_script" -n "$NAMESPACE" --supabase-namespace "$SUPABASE_NAMESPACE" } # ---- 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 " 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 } is_local_mode() { case "${PROLE_MODE:-}" in ""|k3d) return 0 ;; esac return 1 } 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: # Attributes must use double quotes in the XML (as in the sample). get_attr() { # $1 = line, $2 = attribute name # outputs value or empty local val val=$(printf '%s\n' "$1" | sed -n "s/.*$2=\"\([^\"]*\)\".*/\1/p") # Special override for postgres hostPort and namespace from prole.cfg if [ "$2" = "hostPort" ] && [ "$(get_attr "$1" "id")" = "postgres" ] && [ -n "${DB_HOST_PORT:-}" ]; then printf '%s' "$DB_HOST_PORT" return fi if [ "$2" = "namespace" ] && [ "$(get_attr "$1" "id")" = "postgres" ] && [ -n "${NAMESPACE:-}" ]; then printf '%s' "$NAMESPACE" return fi printf '%s' "$val" } foreach_mapping_xml() { # 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.: # 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 if [ $in_comment -eq 1 ]; then case "$line" in *"-->"*) in_comment=0; continue ;; *) continue ;; esac fi case "$line" in *""*) # single-line comment; skip line continue ;; *) in_comment=1 continue ;; esac ;; esac if [ $in_mapping -eq 0 ]; then case "$line" in *"' 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")" # Replace ${NAMESPACE} or ${PROLE_NAMESPACE} if NAMESPACE is set in env/cfg if [ -n "${NAMESPACE:-}" ]; then ns="${ns//\$\{NAMESPACE\}/$NAMESPACE}" ns="${ns//\$\{PROLE_NAMESPACE\}/$NAMESPACE}" fi if [ -n "${PF_MONITORING_NAMESPACE:-}" ]; then ns="${ns//\$\{MONITORING_NAMESPACE\}/$PF_MONITORING_NAMESPACE}" ns="${ns//\$\{PROLE_MONITORING_NAMESPACE\}/$PF_MONITORING_NAMESPACE}" fi if [ -n "${PF_MANAGEMENT_NAMESPACE:-}" ]; then ns="${ns//\$\{MANAGEMENT_NAMESPACE\}/$PF_MANAGEMENT_NAMESPACE}" ns="${ns//\$\{PROLE_MANAGEMENT_NAMESPACE\}/$PF_MANAGEMENT_NAMESPACE}" fi # Mapping-specific overrides (removed hardcoded namespace overrides) if [ "$id" = "postgres" ]; then if [ -n "${DB_HOST_PORT:-}" ]; then hostPort="$DB_HOST_PORT" fi if [ "$SUPABASE_ENABLED_EFFECTIVE" -eq 1 ]; then ns="$SUPABASE_NAMESPACE" target="$SUPABASE_DB_TARGET" fi fi # prole-db logic removed, now uses XML # 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" } foreach_mapping_cfg() { local callback="$1" local prefix mappings line parsed prefix="$(mapping_prefix_for_mode)" if ! mappings="$(collect_cfg_mappings "$prefix")"; then return 1 fi while IFS= read -r line; do [ -z "$line" ] && continue parsed="$(parse_cfg_mapping "${line#*$'\t'}")" local id ns target address hostPort servicePort protocol description IFS=$'\t' read -r id ns target address hostPort servicePort protocol description <<<"$parsed" if [ -z "$id" ] || [ -z "$ns" ] || [ -z "$target" ] || [ -z "$hostPort" ] || [ -z "$servicePort" ]; then err "Invalid mapping (missing required fields): ${line#*$'\t'}" exit 1 fi if [ -n "$TARGET_ID" ] && [ "$id" != "$TARGET_ID" ]; then continue 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" done <<<"$mappings" return 0 } foreach_mapping() { local callback="$1" if foreach_mapping_cfg "$callback"; then return 0 fi if [ -z "${CONFIG_FILE:-}" ]; then err "No port-forward mappings found in prole.cfg and no config file specified." exit 1 fi foreach_mapping_xml "$callback" } build_port_forward_cmd() { # echo a command string local ns="$1" target="$2" address="$3" hostPort="$4" servicePort="$5" # kubectl port-forward -n --address : 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 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 ... : 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 } scan_for_collisions() { local id="$1" ns="$2" target="$3" address="$4" hostPort="$5" servicePort="$6" protocol="$7" description="$8" if port_in_use "$hostPort"; then # find which process is using it local pid_info="" if have lsof; then pid_info=$(lsof -nP -iTCP:"$hostPort" -sTCP:LISTEN -t 2>/dev/null | head -n 1) fi if [ -n "$pid_info" ]; then # Check if this PID is already managed by us local pidfile pid_managed pidfile="$(pid_file_for "$id")" pid_managed="$(read_pid "$pidfile" 2>/dev/null || true)" if [ "$pid_info" = "$pid_managed" ]; then vlog "Port $hostPort is in use by our own process ($id, pid=$pid_info). This is fine for start/restart." return 0 fi local proc_details proc_details=$(ps -p "$pid_info" -o pid=,command= 2>/dev/null | sed 's/[[:space:]]\+/ /g' || echo "$pid_info") if [ "$FORCE" -eq 1 ]; then log "Port $hostPort is in use by: $proc_details" log "Force enabled. Killing process $pid_info..." if ! kill -9 "$pid_info" 2>/dev/null; then err "Failed to kill process $pid_info. Permission denied?" exit 1 fi sleep 0.5 else err "Port collision detected: Port $hostPort is already in use by another process." err "Process details: $proc_details" err "Use -f or --force to kill the offending process, or stop it manually." exit 1 fi else # Port in use but we can't find PID (maybe another user's process) err "Port collision detected: Port $hostPort is in use, but could not determine PID (check with sudo lsof -i :$hostPort)." exit 1 fi fi } do_start() { if ! port_forward_enabled_for_mode; then log "Port forwards disabled for mode '${PROLE_MODE:-auto}'." return 0 fi if ! port_forward_allowed_on_host; then log "Port forwards disabled on this host ($(current_hostname)); not listed in PORT_FORWARD_*_HOSTS." return 0 fi validate_env if is_local_mode; then # Preflight: require Docker daemon and k3d (if applicable) ensure_docker_running ensure_k3d_ready_if_applicable fi ensure_supabase_ports vlog "Scanning for port collisions..." foreach_mapping scan_for_collisions 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() { if ! port_forward_enabled_for_mode; then log "Port forwards disabled for mode '${PROLE_MODE:-auto}'." return 0 fi if ! port_forward_allowed_on_host; then log "Port forwards disabled on this host ($(current_hostname)); not listed in PORT_FORWARD_*_HOSTS." return 0 fi validate_env if is_local_mode; then # Preflight: require Docker daemon and k3d (if applicable) ensure_docker_running ensure_k3d_ready_if_applicable fi ensure_supabase_ports vlog "Scanning for port collisions (excluding our own)..." # During restart, we'll stop them first anyway, but let's be safe. foreach_mapping stop_port_forward foreach_mapping scan_for_collisions foreach_mapping start_port_forward } do_status() { if ! port_forward_allowed_on_host; then log "Port forwards disabled on this host ($(current_hostname)); not listed in PORT_FORWARD_*_HOSTS." return 0 fi 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 -n # 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 ;; -m|--mode) shift prole_set_mode "${1:-}" shift ;; -m=*|--mode=*) prole_set_mode "${1#*=}" shift ;; -v|--verbose) VERBOSE=1 shift ;; -f|--force) FORCE=1 shift ;; -c) shift [ $# -gt 0 ] || { err "-c requires a file path"; usage; exit 2; } CONFIG_FILE="$1" CONFIG_FILE_SOURCE="cli" shift ;; --config-file=*) CONFIG_FILE="${1#*=}" CONFIG_FILE_SOURCE="cli" shift ;; -h|--help) usage exit 0 ;; *) if [ -z "$ACTION" ]; then err "Unknown arg: $1" usage exit 2 fi TARGET_ID="$1" shift ;; esac done if [ "$CONFIG_FILE_SOURCE" = "auto" ]; then CONFIG_FILE="$(resolve_config_file)" fi [ -n "$ACTION" ] || { usage; exit 2; } case "$ACTION" in start) do_start ;; stop) do_stop ;; restart) do_restart ;; status) do_status ;; esac