prole/mock_val/init_port_forwards.sh
chrisfu 3f96f66a78 feat(mock_val): rewrite init scripts; add new service init scripts
Rewrites (updated for knoe namespace, GKE support, and current service configs):
  init_gitlab.sh, init_kong.sh, init_cnpg_backup.sh, init_monitoring.sh,
  init_garage_store.sh, init_gitea.sh, init_kdc.sh, init_openbao.sh,
  init_argocd.sh, init_certmgr.sh, init_common_services.sh, init_db_manager.sh,
  init_forgejo.sh, init_k3s_registry.sh, init_kerberos.sh, init_nginx_ingress.sh,
  init_port_forwards.sh, init_registry.sh, init_service_layer.sh

Deleted: init_cloudnative_pg.sh (superseded by init_cnpg_gke.sh)

New scripts:
  init_cnpg_gke.sh      — CNPG setup for GKE with Workload Identity
  init_knoe_auth.sh     — knoe-auth OIDC service init
  init_knoe_users.sh    — user provisioning
  init_redis.sh         — Redis init
  init_oauth2_proxy.sh / init_oauth2_proxy_prole.sh — OAuth2 proxy setup
  init_grafana_oauth.sh / init_grafana_oauth_prole.sh — Grafana OAuth wiring
  init_1password.sh     — 1Password Connect init
  init_min.sh           — minimal bootstrap

Co-authored-by: Junie <junie@jetbrains.com>
2026-05-23 21:31:27 -07:00

1185 lines
34 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 port-mapping.cfg.
PROG="init_port_forwards"
SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
# shellcheck disable=SC1090
source "$SCRIPT_DIR/knoe_cfg.sh"
KNOE_HOME="${KNOE_HOME:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)}"
VERBOSE=0
PROLE_CFG_OVERRIDE=""
PORT_MAPPING_FILE_PATH=""
PORT_FORWARD_ENABLED="${PORT_FORWARD_ENABLED:-}"
PORT_FORWARD_K3D_ENABLED="${PORT_FORWARD_K3D_ENABLED:-}"
PORT_FORWARD_K3S_ENABLED="${PORT_FORWARD_K3S_ENABLED:-}"
PORT_FORWARD_HOSTS="${PORT_FORWARD_HOSTS:-}"
PORT_FORWARD_K3D_HOSTS="${PORT_FORWARD_K3D_HOSTS:-}"
PORT_FORWARD_K3S_HOSTS="${PORT_FORWARD_K3S_HOSTS:-}"
PORT_MAPPING_FILE="${PORT_MAPPING_FILE:-}"
PORT_FORWARD_SKIP_VALIDATE="${PORT_FORWARD_SKIP_VALIDATE:-}"
PORT_FORWARD_SKIP_WAIT="${PORT_FORWARD_SKIP_WAIT:-}"
usage() {
cat <<EOF
Usage:
$PROG [-v|--verbose] [-f|--force] [-c|--config-file=FILE] <start|stop|restart|status> [component]
Options:
-c, --config-file=FILE Path to a config file to source when generating port-mapping.cfg
-v, --verbose Verbose output
-f, --force Force: kill existing processes blocking ports
Examples:
$PROG -c ./k3d.cfg start
$PROG stop openbao
$PROG --verbose status
Config (k3d.cfg/k3s.cfg/gke.cfg or port-mapping.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
Legacy port-mapping.cfg:
dashboard: local=8000 remote=80 ns=kubernetes-dashboard svc=kubernetes-dashboard-web address=0.0.0.0
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() {
local cfg=""
if declare -F _knoe_cfg_select_cfg_file >/dev/null 2>&1; then
if [ -n "${KNOE_CONF:-}" ]; then
cfg="$(_knoe_cfg_select_cfg_file "$KNOE_CONF")"
if [ -n "$cfg" ]; then
printf '%s' "$cfg"
return 0
fi
fi
if [ -n "${KNOE_HOME:-}" ]; then
cfg="$(_knoe_cfg_select_cfg_file "$KNOE_HOME/conf")"
if [ -n "$cfg" ]; then
printf '%s' "$cfg"
return 0
fi
fi
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 "${KNOE_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_port_mapping_file() {
if [ -n "${PORT_MAPPING_FILE:-}" ]; then
printf '%s' "$PORT_MAPPING_FILE"
return 0
fi
if [ -n "${KNOE_CONF:-}" ]; then
printf '%s/port-mapping.cfg' "$KNOE_CONF"
return 0
fi
if [ -n "${KNOE_HOME:-}" ]; then
printf '%s/conf/port-mapping.cfg' "$KNOE_HOME"
return 0
fi
return 1
}
mapping_prefix_for_mode() {
case "${KNOE_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 file="$2"
[ -f "$file" ] || return 1
awk -v p="$prefix" '
/^[[:space:]]*[#;]/ {next}
/^[[:space:]]*\[/ {next}
{
line=$0
pos=index(line, "=")
if (pos == 0) next
key=substr(line, 1, pos-1)
val=substr(line, pos+1)
gsub(/^[[:space:]]+|[[:space:]]+$/, "", key)
gsub(/^[[:space:]]+|[[:space:]]+$/, "", val)
if (key ~ ("^" p "[0-9]+$")) {
num=substr(key, length(p)+1)+0
print num "\t" val
count++
}
}
END { if (count == 0) exit 1 }
' "$file" | sort -n
}
collect_cfg_mappings_for_mode() {
local file="$1"
local prefix
prefix="$(mapping_prefix_for_mode)"
if collect_cfg_mappings "$prefix" "$file"; then
return 0
fi
# Fallback to generic mappings if mode-specific ones are absent
if collect_cfg_mappings "PORT_FORWARD_MAPPING_" "$file"; then
return 0
fi
return 1
}
has_ini_mappings() {
local file="$1"
[ -f "$file" ] || return 1
grep -Eq '^[[:space:]]*PORT_FORWARD_.*_MAPPING_[0-9]+' "$file"
}
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 "${PROLE_NAMESPACE:-}" ]; then
ns="${ns//\$\{NAMESPACE\}/$PROLE_NAMESPACE}"
ns="${ns//\$\{PROLE_NAMESPACE\}/$PROLE_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"
}
generate_port_mapping_file() {
local src_cfg="$1"
local dst_cfg="$2"
[ -f "$src_cfg" ] || { err "Knoe config file not found: $src_cfg"; return 1; }
local dst_dir
dst_dir="$(dirname "$dst_cfg")"
mkdir -p "$dst_dir" >/dev/null 2>&1 || true
local tmp="${dst_cfg}.tmp.$$"
{
printf '; Port mappings generated by %s\n' "$PROG"
printf '; Source: %s\n' "$src_cfg"
awk '
/^[[:space:]]*[#;]/ {next}
/^[[:space:]]*\[/ {next}
{
line=$0
pos=index(line, "=")
if (pos == 0) next
key=substr(line, 1, pos-1)
val=substr(line, pos+1)
gsub(/^[[:space:]]+|[[:space:]]+$/, "", key)
gsub(/^[[:space:]]+|[[:space:]]+$/, "", val)
if (key ~ /^PORT_FORWARD_.*_MAPPING_[0-9]+$/) {
print key " = " val
}
}
' "$src_cfg"
} >"$tmp"
if ! grep -q "^PORT_FORWARD_.*_MAPPING_[0-9][0-9]*[[:space:]]*=" "$tmp"; then
rm -f "$tmp" >/dev/null 2>&1 || true
err "No port-forward mappings found in $src_cfg"
return 1
fi
mv "$tmp" "$dst_cfg"
return 0
}
ensure_port_mapping_file() {
local src_cfg="$1"
local dst_cfg="$2"
if [ -f "$dst_cfg" ]; then
return 0
fi
if [ -z "$src_cfg" ] || [ ! -f "$src_cfg" ]; then
err "Port mapping file not found: $dst_cfg"
err "No knoe.cfg available to generate it."
return 1
fi
log "Port mapping file not found; generating $dst_cfg from $src_cfg"
generate_port_mapping_file "$src_cfg" "$dst_cfg"
}
port_forward_enabled_for_mode() {
local val=""
case "${KNOE_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_set_default() {
local key="$1"
local val
val="$(cfg_value "$key")"
if [ -n "$val" ]; then
if [ -z "${!key:-}" ] || [ -n "${PROLE_CFG_OVERRIDE:-}" ]; then
printf -v "$key" '%s' "$val"
export "$key"
fi
fi
}
init_cfg_context() {
if [ -n "${CFG_FILE:-}" ] && [ -f "$CFG_FILE" ]; then
cfg_set_default "NAMESPACE"
cfg_set_default "MONITORING_NAMESPACE"
cfg_set_default "MANAGEMENT_NAMESPACE"
cfg_set_default "PROLE_MONITORING_NAMESPACE"
cfg_set_default "PROLE_MANAGEMENT_NAMESPACE"
cfg_set_default "SUPABASE_ENABLED"
cfg_set_default "SUPABASE_NAMESPACE"
cfg_set_default "SUPABASE_DB_SERVICE"
cfg_set_default "DB_HOST_PORT"
cfg_set_default "KNOE_DB_ALT_PORT"
fi
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
KNOE_DB_ALT_PORT_BASE="${KNOE_DB_ALT_PORT_BASE:-15432}"
KNOE_DB_ALT_PORT_EFFECTIVE=""
if [ "$SUPABASE_ENABLED_EFFECTIVE" -eq 1 ]; then
if [ -n "${KNOE_DB_ALT_PORT:-}" ]; then
KNOE_DB_ALT_PORT_EFFECTIVE="$KNOE_DB_ALT_PORT"
else
KNOE_DB_ALT_PORT_EFFECTIVE="$(find_available_port "$KNOE_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 is_truthy "${PORT_FORWARD_SKIP_VALIDATE:-}"; then
return 0
fi
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
}
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 -> knoe-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
}
docker_port_owner() {
local port="$1"
have docker || return 1
docker ps --format '{{.ID}}\t{{.Names}}\t{{.Ports}}' 2>/dev/null | \
awk -v p=":"$port"->" 'index($0, p) {print $1 "\t" $2; exit 0}'
}
should_skip_mapping() {
local id="$1" hostPort="$2"
case "$id" in
registry|openbao)
local owner
owner="$(docker_port_owner "$hostPort")"
if [ -n "$owner" ]; then
log "Skipping $id port-forward; docker is already publishing port $hostPort ($owner)."
return 0
fi
;;
esac
return 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
}
is_local_mode() {
case "${KNOE_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"; }
wait_for_service_endpoints() {
local ns="$1" svc="$2"
local timeout="${PORT_FORWARD_WAIT_TIMEOUT:-120}"
local interval="${PORT_FORWARD_WAIT_INTERVAL:-3}"
local start
start=$(date +%s)
while true; do
if "$KUBECTL" -n "$ns" get endpoints "$svc" -o jsonpath='{.subsets[*].addresses[*].ip}' 2>/dev/null | grep -q .; then
return 0
fi
if (( $(date +%s) - start > timeout )); then
err "WARN: Endpoints for service '$svc' in namespace '$ns' not ready after ${timeout}s; continuing."
return 1
fi
sleep "$interval"
done
}
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
}
is_matching_port_forward() {
local pid="$1" target="$2" hostPort="$3" servicePort="$4"
local cmd
cmd="$(ps -p "$pid" -o command= 2>/dev/null || true)"
if [ -z "$cmd" ]; then
return 1
fi
case "$cmd" in
*port-forward*"$target"*"$hostPort:$servicePort"*) return 0 ;;
esac
return 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
}
handle_mapping_parsed() {
local callback="$1"
local parsed="$2"
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): $parsed"
exit 1
fi
if [ -n "$TARGET_ID" ] && [ "$id" != "$TARGET_ID" ]; then
return 0
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"
return 0
}
foreach_mapping_cfg() {
local callback="$1"
local file="${2:-$PORT_MAPPING_FILE_PATH}"
local mappings line parsed count=0
[ -n "$file" ] || return 1
if ! mappings="$(collect_cfg_mappings_for_mode "$file")"; then
return 1
fi
while IFS= read -r line; do
[ -z "$line" ] && continue
parsed="$(parse_cfg_mapping "${line#*$'\t'}")"
if handle_mapping_parsed "$callback" "$parsed"; then
count=$((count + 1))
fi
done <<<"$mappings"
[ "$count" -gt 0 ] || return 1
return 0
}
foreach_mapping_legacy() {
local callback="$1"
local file="${2:-$PORT_MAPPING_FILE_PATH}"
local line id rest token key value
local hostPort servicePort ns target address protocol description
local count=0
[ -f "$file" ] || return 1
while IFS= read -r line; do
line="$(trim "$line")"
[ -z "$line" ] && continue
case "$line" in \#*|\;*) continue ;; esac
id="${line%%:*}"
rest="${line#*:}"
if [ "$id" = "$line" ]; then
continue
fi
id="$(trim "$id")"
rest="$(trim "$rest")"
hostPort="" servicePort="" ns="" target="" address="" protocol="" description=""
for token in $rest; do
key="$(trim "${token%%=*}")"
value="$(trim "${token#*=}")"
case "$key" in
local|host|hostPort|host_port) hostPort="$value" ;;
remote|servicePort|service_port) servicePort="$value" ;;
ns|namespace) ns="$value" ;;
svc|service) target="svc/$value" ;;
target) target="$value" ;;
address|addr) address="$value" ;;
protocol|proto) protocol="$value" ;;
desc|description) description="$value" ;;
esac
done
if [ -z "$address" ]; then address="127.0.0.1"; fi
if [ -z "$protocol" ]; then protocol="TCP"; fi
line="id=$id;namespace=$ns;target=$target;address=$address;hostPort=$hostPort;servicePort=$servicePort;protocol=$protocol;description=$description"
parsed="$(parse_cfg_mapping "$line")"
if handle_mapping_parsed "$callback" "$parsed"; then
count=$((count + 1))
fi
done <"$file"
[ "$count" -gt 0 ] || return 1
return 0
}
foreach_mapping() {
local callback="$1"
if [ -n "${CFG_FILE:-}" ] && [ -f "$CFG_FILE" ]; then
if foreach_mapping_cfg "$callback" "$CFG_FILE"; then
return 0
fi
fi
if [ -n "${PORT_MAPPING_FILE_PATH:-}" ] && [ -f "$PORT_MAPPING_FILE_PATH" ]; then
if has_ini_mappings "$PORT_MAPPING_FILE_PATH"; then
if foreach_mapping_cfg "$callback" "$PORT_MAPPING_FILE_PATH"; then
return 0
fi
else
if foreach_mapping_legacy "$callback" "$PORT_MAPPING_FILE_PATH"; then
return 0
fi
fi
fi
return 1
}
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
if should_skip_mapping "$id" "$hostPort"; then
stop_port_forward "$id" "$ns" "$target" "$address" "$hostPort" "$servicePort" "$protocol" "$description"
return 0
fi
# Aggressively stop existing processes before starting to avoid port conflicts
stop_port_forward "$id" "$ns" "$target" "$address" "$hostPort" "$servicePort" "$protocol" "$description"
# If target is a service, wait briefly for endpoints to be ready to avoid immediate port-forward failures.
if [[ "$target" == svc/* ]] && ! is_truthy "${PORT_FORWARD_SKIP_WAIT:-}"; then
wait_for_service_endpoints "$ns" "${target#svc/}" || true
fi
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
}
scan_for_collisions() {
local id="$1" ns="$2" target="$3" address="$4" hostPort="$5" servicePort="$6" protocol="$7" description="$8"
if should_skip_mapping "$id" "$hostPort"; then
return 0
fi
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
if is_matching_port_forward "$pid_info" "$target" "$hostPort" "$servicePort"; then
vlog "Port $hostPort already forwarded by kubectl for $id (pid=$pid_info)."
return 0
fi
# 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 '${KNOE_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..."
if ! foreach_mapping scan_for_collisions; then
err "No port-forward mappings found. Check ${CFG_FILE:-knoe.cfg} or ${PORT_MAPPING_FILE_PATH:-port-mapping.cfg}."
exit 1
fi
if ! foreach_mapping start_port_forward; then
err "No port-forward mappings started. Check ${CFG_FILE:-knoe.cfg} or ${PORT_MAPPING_FILE_PATH:-port-mapping.cfg}."
exit 1
fi
}
do_stop() {
# stop doesn't require cluster access, but it does need state dirs
ensure_dirs
if ! foreach_mapping stop_port_forward; then
log "No port-forward mappings found to stop."
fi
}
do_restart() {
if ! port_forward_enabled_for_mode; then
log "Port forwards disabled for mode '${KNOE_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.
if ! foreach_mapping stop_port_forward; then
err "No port-forward mappings found. Check ${CFG_FILE:-knoe.cfg} or ${PORT_MAPPING_FILE_PATH:-port-mapping.cfg}."
exit 1
fi
if ! foreach_mapping scan_for_collisions; then
err "No port-forward mappings found. Check ${CFG_FILE:-knoe.cfg} or ${PORT_MAPPING_FILE_PATH:-port-mapping.cfg}."
exit 1
fi
if ! foreach_mapping start_port_forward; then
err "No port-forward mappings started. Check ${CFG_FILE:-knoe.cfg} or ${PORT_MAPPING_FILE_PATH:-port-mapping.cfg}."
exit 1
fi
}
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 =="
if ! foreach_mapping status_one; then
log " (no port-forward mappings found)"
fi
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
;;
-m|--mode)
shift
knoe_set_mode "${1:-}"
shift
;;
-m=*|--mode=*)
knoe_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; }
PROLE_CFG_OVERRIDE="$1"
shift
;;
--config-file=*)
PROLE_CFG_OVERRIDE="${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
if [ -n "$PROLE_CFG_OVERRIDE" ]; then
CFG_FILE="$PROLE_CFG_OVERRIDE"
if [ -f "$CFG_FILE" ]; then
KNOE_CONF="$(cd "$(dirname "$CFG_FILE")" && pwd)"
export KNOE_CONF
fi
fi
if [ -z "${CFG_FILE:-}" ]; then
CFG_FILE="$(cfg_file || true)"
fi
[ -n "$ACTION" ] || { usage; exit 2; }
PORT_MAPPING_FILE_PATH="$(resolve_port_mapping_file || true)"
init_cfg_context
if ! ensure_port_mapping_file "$CFG_FILE" "$PORT_MAPPING_FILE_PATH"; then
exit 1
fi
case "$ACTION" in
start) do_start ;;
stop) do_stop ;;
restart) do_restart ;;
status) do_status ;;
esac