prole/etc/prole_cfg.sh
chrisfu b114099e17 Use prole.cfg hostnames for Supabase front-door
- Persist and export supabase_hostname for canonical external Supabase entrypoint (db.prole.org)

- Render Supabase Helm ingress host and public URLs (API_EXTERNAL_URL/GOTRUE_SITE_URL/SUPABASE_PUBLIC_URL) from config

- Align tracked Traefik ingress manifests to db.prole.org

- Generalize service hostname/TLS wiring in k3s automation and refresh Kong/monitoring behavior

- Make optional workload policy checks deterministic when kube context is absent; add render tests
2026-03-19 04:19:38 -07:00

1163 lines
36 KiB
Bash

#!/usr/bin/env bash
# Standard include for Prole etc scripts.
# - Loads environment from env.sh (if available)
# - Loads values from PROLE_CONF/prole.cfg (INI-style)
# - Does not override already-set env vars
if [[ "${_PROLE_CFG_LOADED:-}" == "1" ]]; then
return 0 2>/dev/null || true
fi
_PROLE_CFG_LOADED=1
_prole_cfg_script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
_prole_cfg_home_guess=$(cd "$_prole_cfg_script_dir/.." && pwd)
# 1. Try to load env.sh to establish base PROLE_HOME/PROLE_CONF
if [[ -n "${PROLE_HOME:-}" && -f "$PROLE_HOME/env.sh" ]]; then
# shellcheck disable=SC1090
source "$PROLE_HOME/env.sh"
elif [[ -f "$HOME/.prole/env.sh" ]]; then
# shellcheck disable=SC1090
source "$HOME/.prole/env.sh"
elif [[ -f "$_prole_cfg_home_guess/env.sh" ]]; then
# shellcheck disable=SC1090
source "$_prole_cfg_home_guess/env.sh"
fi
_prole_trim() {
local s="$1"
s="${s#"${s%%[![:space:]]*}"}"
s="${s%"${s##*[![:space:]]}"}"
printf '%s' "$s"
}
_prole_cfg_expand() {
local value="$1"
local out="" rest="$value"
local prefix token suffix var repl
if [[ -z "$value" ]]; then
printf '%s' "$value"
return 0
fi
case "$value" in
'${OPENBAO:'*|'${PROLE_SECRET:'*)
printf '%s' "$value"
return 0
;;
esac
while [[ "$rest" =~ ^([^$]*)(\$[A-Za-z_][A-Za-z0-9_]*|\$\{[A-Za-z_][A-Za-z0-9_]*\})(.*)$ ]]; do
prefix="${BASH_REMATCH[1]}"
token="${BASH_REMATCH[2]}"
suffix="${BASH_REMATCH[3]}"
var="${token#\$}"
var="${var#\{}"
var="${var%\}}"
if [[ -n "${!var+x}" ]]; then
repl="${!var}"
else
repl="$token"
fi
out+="${prefix}${repl}"
rest="$suffix"
done
out+="$rest"
printf '%s' "$out"
}
_prole_host_from_url() {
local val="${1:-}"
val="${val#http://}"
val="${val#https://}"
val="${val%%/*}"
val="${val%%:*}"
printf '%s' "$val"
}
prole_is_in_cluster() {
[[ -n "${KUBERNETES_SERVICE_HOST:-}" && -f /var/run/secrets/kubernetes.io/serviceaccount/token ]]
}
_prole_local_registry_enabled() {
local raw="${PROLE_ENABLE_LOCAL_REGISTRY:-${ENABLE_LOCAL_REGISTRY:-}}"
if [[ -n "$raw" ]]; then
_prole_bool_true "$raw" && return 0
return 1
fi
local mode=""
if command -v prole_normalize_mode >/dev/null 2>&1; then
mode=$(prole_normalize_mode "${PROLE_MODE:-${DEPLOYMENT_MODE:-${CLUSTER_ENV:-}}}")
else
mode="${PROLE_MODE:-${DEPLOYMENT_MODE:-${CLUSTER_ENV:-}}}"
fi
[[ "$mode" == "k3d" || "$mode" == "k3s" ]]
}
_prole_bool_true() {
case "${1:-}" in
1|true|TRUE|True|yes|YES|Yes|on|ON|On) return 0 ;;
esac
return 1
}
_prole_kubeconfig_mark_insecure() {
local cfg="${1:-${KUBECONFIG:-}}"
[[ -z "$cfg" || ! -f "$cfg" ]] && return 1
command -v kubectl >/dev/null 2>&1 || return 1
local clusters
clusters=$(KUBECONFIG="$cfg" kubectl config get-clusters 2>/dev/null | awk 'NR>1 {print $1}' || true)
[[ -z "$clusters" ]] && return 1
local c
for c in $clusters; do
KUBECONFIG="$cfg" kubectl config set-cluster "$c" --insecure-skip-tls-verify=true >/dev/null 2>&1 || true
done
return 0
}
_PROLE_CFG_SET_VARS="|"
_prole_cfg_set_default() {
local key="$1" value="$2" token="|$key|"
# Only set if currently empty OR if we were the ones who set it from config previously
if [[ -z "${!key:-}" || "$_PROLE_CFG_SET_VARS" == *"$token"* ]]; then
printf -v "$key" '%s' "$value"
export "$key"
if [[ "$_PROLE_CFG_SET_VARS" != *"$token"* ]]; then
_PROLE_CFG_SET_VARS="${_PROLE_CFG_SET_VARS}${key}|"
fi
fi
}
_prole_read_cfg() {
local cfg="$1" line key value
while IFS= read -r line || [[ -n "$line" ]]; do
line="$(_prole_trim "$line")"
[[ -z "$line" ]] && continue
case "$line" in
\#*|\;*|\[*\])
continue
;;
esac
if [[ "$line" == *"="* ]]; then
key="$(_prole_trim "${line%%=*}")"
value="$(_prole_trim "${line#*=}")"
[[ -z "$key" ]] && continue
# Replace dots with underscores for shell compatibility
key="${key//./_}"
value="$(_prole_cfg_expand "$value")"
_prole_cfg_set_default "$key" "$value"
fi
done <"$cfg"
}
_prole_cfg_extract_key() {
local cfg="$1" key="$2"
local value
value=$(awk -F= -v k="$key" '
/^[[:space:]]*;/ {next}
/^[[:space:]]*#/ {next}
/^[[:space:]]*\[/ {next}
$1 ~ "^[[:space:]]*" k "[[:space:]]*$" {
v=$2
sub(/^[[:space:]]+/, "", v)
sub(/[[:space:]]+$/, "", v)
val=v
}
END { print val }
' "$cfg" 2>/dev/null || true)
value="$(_prole_cfg_expand "$value")"
printf '%s' "$value"
}
_prole_cfg_extract_key_in_files() {
local key="$1"; shift
local value
if [[ $# -lt 1 ]]; then
return 0
fi
value=$(awk -F= -v k="$key" '
/^[[:space:]]*;/ {next}
/^[[:space:]]*#/ {next}
/^[[:space:]]*\[/ {next}
$1 ~ "^[[:space:]]*" k "[[:space:]]*$" {
v=$2
sub(/^[[:space:]]+/, "", v)
sub(/[[:space:]]+$/, "", v)
val=v
}
END { print val }
' "$@" 2>/dev/null || true)
value="$(_prole_cfg_expand "$value")"
printf '%s' "$value"
}
_prole_cfg_realpath() {
local p="$1"
if command -v python3 >/dev/null 2>&1; then
python3 -c 'import os,sys; print(os.path.realpath(sys.argv[1]))' "$p" 2>/dev/null && return 0
fi
if command -v python >/dev/null 2>&1; then
python -c 'import os,sys; print(os.path.realpath(sys.argv[1]))' "$p" 2>/dev/null && return 0
fi
# Fall back to the raw path.
printf '%s' "$p"
}
_prole_cfg_layer_files() {
local entry="$1"
[[ -z "$entry" ]] && return 0
local resolved env_dir env_name base f
resolved=$(_prole_cfg_realpath "$entry")
env_dir=$(cd "$(dirname "$resolved")" 2>/dev/null && pwd || true)
env_name=$(basename "$env_dir")
base="$env_dir/prole.cfg"
if [[ "$env_name" =~ ^(dev|service|prod|test)$ && -f "$base" ]]; then
printf '%s\n' "$base"
for f in "$env_dir"/*.cfg; do
[[ -f "$f" ]] || continue
[[ "$(basename "$f")" == "prole.cfg" ]] && continue
[[ "$(basename "$f")" == .* ]] && continue
printf '%s\n' "$f"
done
return 0
fi
printf '%s\n' "$entry"
}
_prole_cfg_env_from_hint() {
local s
s=$(printf '%s' "${1:-}" | tr 'A-Z' 'a-z')
case "$s" in
dev|k3d|k3d-*|prole-dev-*|knoe-dev-cluster|k3d-knoe-*) printf '%s' "dev"; return 0 ;;
service|k3s|k3s-*|prole-service-*|knoe-system|k3d-knoe-system) printf '%s' "service"; return 0 ;;
prod|production|k8s|k8s-*|prole-prod-*) printf '%s' "prod"; return 0 ;;
test|testing) printf '%s' "test"; return 0 ;;
esac
printf '%s' ""
}
_prole_cfg_default_ns_for_env() {
case "${1:-}" in
test) printf '%s' "prole-test" ;;
*) printf '%s' "prole-db" ;;
esac
}
_prole_cfg_default_sns_for_env() {
case "${1:-}" in
dev) printf '%s' "default" ;;
test) printf '%s' "prole-test" ;;
*) printf '%s' "knoe-system" ;;
esac
}
_prole_cfg_write_default_base() {
local base="$1" env="$2"
local ns sns
ns="$(_prole_cfg_default_ns_for_env "$env")"
sns="$(_prole_cfg_default_sns_for_env "$env")"
mkdir -p "$(dirname "$base")" 2>/dev/null || true
cat >"$base" <<EOF
; Prole Master Configuration File
; Generated by prole_cfg.sh
[Global]
CLUSTER_ENV = ${env}
NAMESPACE = ${ns}
SERVICE_NAMESPACE = ${sns}
EOF
}
_prole_cfg_bootstrap_env_layout() {
local conf_dir="$1"
local entry="$conf_dir/prole.cfg"
[[ -z "$conf_dir" ]] && return 0
mkdir -p "$conf_dir/dev" "$conf_dir/service" "$conf_dir/prod" "$conf_dir/test" 2>/dev/null || true
# If we already have a valid symlink entrypoint, nothing to do.
if [[ -L "$entry" && -e "$entry" ]]; then
return 0
fi
local env_hint=""
local env=""
# If entry is a legacy regular file, infer env from its contents.
if [[ -f "$entry" && ! -L "$entry" ]]; then
env_hint="$(_prole_cfg_extract_key "$entry" "DEPLOYMENT_MODE")"
env="$(_prole_cfg_env_from_hint "$env_hint")"
if [[ -z "$env" ]]; then
env_hint="$(_prole_cfg_extract_key "$entry" "CLUSTER_ENV")"
env="$(_prole_cfg_env_from_hint "$env_hint")"
fi
if [[ -z "$env" ]]; then
env_hint="$(_prole_cfg_extract_key "$entry" "ENVIRONMENT")"
env="$(_prole_cfg_env_from_hint "$env_hint")"
fi
if [[ -z "$env" ]]; then
# Legacy key used by older configs.
env_hint="$(_prole_cfg_extract_key "$entry" "prole.mode")"
env="$(_prole_cfg_env_from_hint "$env_hint")"
fi
fi
# If still unknown, fall back to any already-exported mode/cluster hint.
if [[ -z "$env" ]]; then
env_hint="${PROLE_MODE:-${DEPLOYMENT_MODE:-${CLUSTER_ENV:-}}}"
if command -v prole_normalize_mode >/dev/null 2>&1; then
env_hint=$(prole_normalize_mode "$env_hint")
fi
env="$(_prole_cfg_env_from_hint "$env_hint")"
fi
[[ -z "$env" ]] && env="dev"
local base="$conf_dir/$env/prole.cfg"
# Heal broken symlink.
if [[ -L "$entry" && ! -e "$entry" ]]; then
[[ -f "$base" ]] || _prole_cfg_write_default_base "$base" "$env"
ln -snf "$env/prole.cfg" "$entry" 2>/dev/null || true
return 0
fi
# Migrate legacy regular-file entrypoint into env base.
if [[ -f "$entry" && ! -L "$entry" ]]; then
if [[ ! -f "$base" ]] || grep -q "Generated by prole_conf" "$base" 2>/dev/null || grep -q "Generated by prole_cfg.sh" "$base" 2>/dev/null; then
cp "$entry" "$base" 2>/dev/null || true
fi
local ts legacy
ts=$(date +%s)
legacy="$conf_dir/prole.cfg.legacy.${ts}"
mv "$entry" "$legacy" 2>/dev/null || true
ln -snf "$env/prole.cfg" "$entry" 2>/dev/null || true
return 0
fi
# If entrypoint is missing, create a base and link it.
if [[ ! -e "$entry" ]]; then
[[ -f "$base" ]] || _prole_cfg_write_default_base "$base" "$env"
ln -snf "$env/prole.cfg" "$entry" 2>/dev/null || true
fi
}
prole_normalize_mode() {
local s
s=$(printf '%s' "${1:-}" | tr 'A-Z' 'a-z')
case "$s" in
dev|k3d|k3d-*) echo "k3d"; return 0 ;;
service|k3s|k3s-*) echo "k3s"; return 0 ;;
prod|production|k8s|k8s-*) echo "k8s"; return 0 ;;
prole-dev-cluster|k3d-prole-dev-cluster) echo "k3d"; return 0 ;;
knoe-dev-cluster|k3d-knoe-dev-cluster) echo "k3d"; return 0 ;;
prole-service-cluster) echo "k3s"; return 0 ;;
prole-prod-cluster) echo "k8s"; return 0 ;;
esac
echo "$s"
}
prole_set_mode() {
local raw="$1"
if [[ -z "$raw" ]]; then
echo "ERROR: --mode requires a value (k3d, k3s, k8s)" >&2
exit 2
fi
local norm
norm=$(prole_normalize_mode "$raw")
case "$norm" in
k3d|k3s|k8s)
PROLE_MODE="$norm"
export PROLE_MODE
;;
*)
echo "ERROR: Unsupported mode '$raw' (use k3d, k3s, or k8s)" >&2
exit 2
;;
esac
}
prole_render_manifest() {
local src="$1"
if [[ "${PROLE_MODE:-}" != "k3d" ]]; then
cat "$src"
return 0
fi
local renderer=""
if [[ -n "${PROLE_SERVICE:-}" && -f "$PROLE_SERVICE/render_manifest.py" ]]; then
renderer="$PROLE_SERVICE/render_manifest.py"
elif [[ -n "${PROLE_HOME:-}" && -f "$PROLE_HOME/etc/render_manifest.py" ]]; then
renderer="$PROLE_HOME/etc/render_manifest.py"
fi
if [[ -n "$renderer" ]]; then
python3 "$renderer" "$src"
return $?
fi
cat "$src"
}
_prole_cfg_file=""
if [[ -n "${PROLE_CONF:-}" && -f "$PROLE_CONF/prole.cfg" ]]; then
_prole_cfg_file="$PROLE_CONF/prole.cfg"
elif [[ -n "${PROLE_HOME:-}" && -f "$PROLE_HOME/conf/prole.cfg" ]]; then
_prole_cfg_file="$PROLE_HOME/conf/prole.cfg"
elif [[ -f "$_prole_cfg_home_guess/conf/prole.cfg" ]]; then
_prole_cfg_file="$_prole_cfg_home_guess/conf/prole.cfg"
fi
if [[ -n "$_prole_cfg_file" ]]; then
if [[ -z "${PROLE_CONF:-}" ]]; then
PROLE_CONF=$(cd "$(dirname "$_prole_cfg_file")" && pwd)
export PROLE_CONF
fi
# Ensure env-dir layout exists and a stable entrypoint symlink is active.
_prole_cfg_bootstrap_env_layout "$PROLE_CONF"
_prole_cfg_file="$PROLE_CONF/prole.cfg"
_prole_cfg_files=()
while IFS= read -r _prole_cfg_f || [[ -n "$_prole_cfg_f" ]]; do
[[ -n "$_prole_cfg_f" ]] || continue
_prole_cfg_files+=("$_prole_cfg_f")
done < <(_prole_cfg_layer_files "$_prole_cfg_file")
if [[ ${#_prole_cfg_files[@]} -eq 0 ]]; then
_prole_cfg_files=("$_prole_cfg_file")
fi
for _prole_cfg_f in "${_prole_cfg_files[@]}"; do
[[ -f "$_prole_cfg_f" ]] || continue
_prole_read_cfg "$_prole_cfg_f"
done
if [[ -z "${KERBEROS_ENABLED:-}" && -n "${ENABLED:-}" ]]; then
KERBEROS_ENABLED="$ENABLED"
export KERBEROS_ENABLED
fi
# Always prefer namespace from prole.cfg (single source of truth).
_cfg_ns=$(_prole_cfg_extract_key_in_files "NAMESPACE" "${_prole_cfg_files[@]}")
if [[ -n "$_cfg_ns" ]]; then
export PROLE_NAMESPACE="$_cfg_ns"
fi
_cfg_sns=$(_prole_cfg_extract_key_in_files "SERVICE_NAMESPACE" "${_prole_cfg_files[@]}")
if [[ -n "$_cfg_sns" ]]; then
export SERVICE_NAMESPACE="$_cfg_sns"
fi
_cfg_sh=$(_prole_cfg_extract_key_in_files "SERVICE_HOSTNAME" "${_prole_cfg_files[@]}")
if [[ -z "$_cfg_sh" ]]; then
_cfg_sh=$(_prole_cfg_extract_key_in_files "service_hostname" "${_prole_cfg_files[@]}")
fi
if [[ -n "$_cfg_sh" ]]; then
export SERVICE_HOSTNAME="$_cfg_sh"
fi
# Supabase front-door hostname (canonical external entrypoint).
_cfg_sbh=$(_prole_cfg_extract_key_in_files "supabase_hostname" "${_prole_cfg_files[@]}")
if [[ -z "$_cfg_sbh" ]]; then
_cfg_sbh=$(_prole_cfg_extract_key_in_files "SUPABASE_HOSTNAME" "${_prole_cfg_files[@]}")
fi
if [[ -n "$_cfg_sbh" ]]; then
export supabase_hostname="$_cfg_sbh"
export SUPABASE_HOSTNAME="$_cfg_sbh"
fi
_cfg_ctx=$(_prole_cfg_extract_key_in_files "KUBECONTEXT" "${_prole_cfg_files[@]}")
if [[ -n "$_cfg_ctx" ]]; then
export KUBECONTEXT="$_cfg_ctx"
fi
unset _cfg_ns _cfg_sns _cfg_sh _cfg_sbh _cfg_ctx _prole_cfg_files _prole_cfg_f
fi
if [[ -z "${PROLE_HOME:-}" && -d "$_prole_cfg_home_guess" ]]; then
PROLE_HOME="$_prole_cfg_home_guess"
export PROLE_HOME
fi
# Final check for critical variables
if [[ -z "${PROLE_SERVICE:-}" && -n "${PROLE_HOME:-}" ]]; then
export PROLE_SERVICE="$PROLE_HOME/etc"
fi
# PROLE_NAMESPACE must be set from prole.cfg only — never from environment or kubectl context
if [[ -n "${PROLE_NAMESPACE:-}" ]]; then
export PROLE_NAMESPACE
fi
if [[ -z "${SERVICE_NAMESPACE:-}" ]]; then
if [[ -n "${PROLE_NAMESPACE:-}" ]]; then
SERVICE_NAMESPACE="$PROLE_NAMESPACE"
export SERVICE_NAMESPACE
fi
fi
if [[ -z "${PROLE_MODE:-}" ]]; then
if [[ -n "${prole_mode:-}" ]]; then
_prole_mode_guess=$(prole_normalize_mode "$prole_mode")
case "$_prole_mode_guess" in
k3d|k3s|k8s) PROLE_MODE="$_prole_mode_guess"; export PROLE_MODE ;;
esac
unset _prole_mode_guess
elif [[ -n "${DEPLOYMENT_MODE:-}" ]]; then
_prole_mode_guess=$(prole_normalize_mode "$DEPLOYMENT_MODE")
case "$_prole_mode_guess" in
k3d|k3s|k8s) PROLE_MODE="$_prole_mode_guess"; export PROLE_MODE ;;
esac
unset _prole_mode_guess
elif [[ -n "${CLUSTER_ENV:-}" ]]; then
_prole_mode_guess=$(prole_normalize_mode "$CLUSTER_ENV")
case "$_prole_mode_guess" in
k3d|k3s|k8s) PROLE_MODE="$_prole_mode_guess"; export PROLE_MODE ;;
esac
unset _prole_mode_guess
fi
fi
if [[ -n "${PROLE_NAMESPACE:-}" ]]; then
_prole_mode_resolved=$(prole_normalize_mode "${PROLE_MODE:-}")
case "$_prole_mode_resolved" in
k3d)
if [[ -n "${LOCAL_REGISTRY_INTERNAL:-}" ]]; then
if [[ "${LOCAL_REGISTRY_INTERNAL}" == *.localhost:5000 ]]; then
LOCAL_REGISTRY_INTERNAL="${LOCAL_REGISTRY_INTERNAL%.localhost:5000}:5000"
export LOCAL_REGISTRY_INTERNAL
elif [[ "${LOCAL_REGISTRY_INTERNAL}" == *.localhost ]]; then
LOCAL_REGISTRY_INTERNAL="${LOCAL_REGISTRY_INTERNAL%.localhost}"
export LOCAL_REGISTRY_INTERNAL
fi
elif _prole_local_registry_enabled; then
LOCAL_REGISTRY_INTERNAL="k3d-prole-registry:5000"
export LOCAL_REGISTRY_INTERNAL
fi
;;
esac
case "$_prole_mode_resolved" in
k3s|k8s)
# Registry namespace is driven by config (SERVICE_NAMESPACE) unless explicitly overridden.
_prole_registry_ns="${REGISTRY_NAMESPACE:-${SERVICE_NAMESPACE:-${PROLE_NAMESPACE:-}}}"
if [[ -n "${LOCAL_REGISTRY_INTERNAL:-}" ]]; then
# Values like "<container>.localhost:5000" are host-only (k3d) and are not reachable from
# k3s/k8s nodes. Treat them as unset so we can auto-resolve a usable in-cluster registry.
if [[ "${LOCAL_REGISTRY_INTERNAL}" == *.localhost:5000 || "${LOCAL_REGISTRY_INTERNAL}" == *.localhost ]]; then
unset LOCAL_REGISTRY_INTERNAL
fi
fi
if [[ -n "${LOCAL_REGISTRY_INTERNAL:-}" ]]; then
:
elif _prole_local_registry_enabled; then
_prole_registry_host="$(_prole_host_from_url "${PROLE_K3S_SERVER:-${K3S_SERVER_URL:-}}")"
if [[ -n "${_prole_registry_host:-}" ]]; then
LOCAL_REGISTRY_INTERNAL="${_prole_registry_host}:5000"
export LOCAL_REGISTRY_INTERNAL
elif [[ -n "${_prole_registry_ns:-}" ]]; then
LOCAL_REGISTRY_INTERNAL="registry.${_prole_registry_ns}.svc.cluster.local:5000"
export LOCAL_REGISTRY_INTERNAL
fi
unset _prole_registry_host
fi
unset _prole_registry_ns
;;
esac
unset _prole_mode_resolved
fi
_prole_is_secret_ref() {
case "${1:-}" in
'${PROLE_SECRET:'*|'${OPENBAO:'*) return 0 ;;
esac
return 1
}
_prole_is_openbao_ref() {
case "${1:-}" in
'${OPENBAO:'*) return 0 ;;
esac
return 1
}
_prole_decrypt_prole_secret() {
local value="${1:-}"
case "$value" in
'${PROLE_SECRET:'*) ;;
*) printf '%s' "$value"; return 0 ;;
esac
python3 - "$value" <<'PY'
import base64
import getpass
import platform
import subprocess
import sys
from pathlib import Path
PROLE_SECRET_PREFIX = "${PROLE_SECRET:"
PROLE_SECRET_SUFFIX = "}"
PROLE_SECRET_VERSION = "v1"
PROLE_SECRET_SERVICE = "prole-installer"
PROLE_SECRET_KEY_FILE = Path.home() / ".prole" / "secrets" / "installer.key"
def get_keychain_key(service: str, account: str) -> bytes:
try:
res = subprocess.run(
["security", "find-generic-password", "-a", account, "-s", service, "-w"],
capture_output=True,
text=True,
)
if res.returncode == 0 and res.stdout.strip():
return base64.urlsafe_b64decode(res.stdout.strip().encode("utf-8"))
except Exception:
pass
return b""
def get_file_key(path: Path) -> bytes:
if not path.exists():
return b""
raw = path.read_text().strip()
try:
return base64.urlsafe_b64decode(raw.encode("utf-8"))
except Exception:
return b""
def get_secret_key() -> bytes:
system = platform.system()
account = getpass.getuser() or "prole"
if system == "Darwin":
return get_keychain_key(PROLE_SECRET_SERVICE, account)
return get_file_key(PROLE_SECRET_KEY_FILE)
def decrypt_prole_secret(value: str) -> str:
if not (value.startswith(PROLE_SECRET_PREFIX) and value.endswith(PROLE_SECRET_SUFFIX)):
return value
inner = value[len(PROLE_SECRET_PREFIX):-len(PROLE_SECRET_SUFFIX)]
parts = inner.split(":")
if len(parts) != 3 or parts[0] != PROLE_SECRET_VERSION:
return ""
key = get_secret_key()
if not key:
return ""
try:
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
nonce = base64.urlsafe_b64decode(parts[1].encode("utf-8"))
ciphertext = base64.urlsafe_b64decode(parts[2].encode("utf-8"))
aesgcm = AESGCM(key)
return aesgcm.decrypt(nonce, ciphertext, None).decode("utf-8")
except Exception:
return ""
value = sys.argv[1]
out = decrypt_prole_secret(value)
print(out)
PY
}
_prole_resolve_openbao_ref() {
local value="${1:-}"
case "$value" in
'${OPENBAO:'*) ;;
*) printf '%s' "$value"; return 0 ;;
esac
python3 - "$value" <<'PY'
import json
import os
import sys
import urllib.request
import urllib.parse
OPENBAO_PREFIX = "${OPENBAO:"
OPENBAO_SUFFIX = "}"
def resolve_openbao_ref(value: str) -> str:
if not (value.startswith(OPENBAO_PREFIX) and value.endswith(OPENBAO_SUFFIX)):
return value
inner = value[len(OPENBAO_PREFIX):-len(OPENBAO_SUFFIX)]
if "#" not in inner:
return ""
path, key = inner.split("#", 1)
if not path or not key:
return ""
mount = "kv"
secret_path = path
if "/" in path:
mount, secret_path = path.split("/", 1)
token = os.environ.get("OPENBAO_ROOT_TOKEN", "")
if not token:
prole_service = os.environ.get("PROLE_SERVICE", "")
if prole_service:
token_path = os.path.join(prole_service, "secrets", "openbao-root-token")
try:
if os.path.exists(token_path):
token = open(token_path, "r", encoding="utf-8").read().strip()
except Exception:
token = ""
if not token:
return ""
url = os.environ.get("PROLE_OPENBAO_URL")
if not url:
# In k3s mode, do NOT fall back to localhost/port-forward. Derive the external
# OpenBao URL from the k3s API server URL when available.
mode = (os.environ.get("PROLE_MODE") or os.environ.get("DEPLOYMENT_MODE") or "").strip().lower()
if mode == "k3s":
k3s_server = (os.environ.get("PROLE_K3S_SERVER") or os.environ.get("K3S_SERVER_URL") or "").strip()
if k3s_server:
try:
parsed = urllib.parse.urlparse(k3s_server)
host = parsed.hostname or ""
if host:
url = f"http://{host}:8200"
except Exception:
url = ""
if not url:
for p in ["8200", "8200"]:
try:
with urllib.request.urlopen(f"http://127.0.0.1:{p}/v1/sys/health", timeout=0.5) as r:
if r.getcode() == 200:
url = f"http://127.0.0.1:{p}"
break
except Exception:
pass
if not url:
mode = (os.environ.get("PROLE_MODE") or os.environ.get("DEPLOYMENT_MODE") or "").strip().lower()
if mode != "k3s":
url = "http://127.0.0.1:8200"
else:
return ""
url = url.rstrip("/")
try:
req = urllib.request.Request(f"{url}/v1/{mount}/data/{secret_path}")
req.add_header("X-Vault-Token", token)
with urllib.request.urlopen(req, timeout=4) as resp:
payload = json.loads(resp.read().decode("utf-8"))
return payload.get("data", {}).get("data", {}).get(key, "") or ""
except Exception:
return ""
value = sys.argv[1]
print(resolve_openbao_ref(value))
PY
}
_prole_usable_dir() {
local dir="$1"
if [[ -z "$dir" ]]; then return 1; fi
if [[ -w "$dir" ]] || [[ ! -e "$dir" && -w "$(dirname "$dir" 2>/dev/null)" ]]; then
printf '%s' "$dir"
return 0
fi
return 1
}
_prole_kubeconfig_path() {
local base=""
# Try PROLE_SERVICE, then PROLE_HOME, then guess
local candidates=("${PROLE_SERVICE:-}/secrets" "${PROLE_HOME:-}" "${_prole_cfg_home_guess:-}")
for b in "${candidates[@]}"; do
if [[ -z "$b" ]]; then continue; fi
if _prole_usable_dir "$b" >/dev/null; then
base="$b"
break
fi
done
# Fallback to ~/.prole if everything else failed
if [[ -z "$base" ]]; then
base="$HOME/.prole/secrets"
mkdir -p "$base" 2>/dev/null || true
fi
if [[ "$base" == */secrets ]]; then
printf '%s\n' "${base}/k3s.kubeconfig"
else
printf '%s\n' "${base}/prole-k3s.kubeconfig"
fi
}
_prole_token_is_bearer() {
local token="$1"
[[ -z "$token" ]] && return 1
# K3s node tokens include '::' and are not valid API bearer tokens.
[[ "$token" == *"::"* ]] && return 1
# JWT-style tokens contain two dots.
case "$token" in
*.*.*) return 0 ;;
esac
# Bootstrap tokens are usually id.secret with a dot separator.
if [[ "$token" == *.* ]]; then
local id="${token%%.*}"
local secret="${token#*.}"
if [[ ${#id} -ge 6 && ${#secret} -ge 16 ]]; then
return 0
fi
fi
return 1
}
_prole_kubeconfig_from_token() {
local server token ns
server="${PROLE_K3S_SERVER:-${K3S_SERVER_URL:-}}"
token="${PROLE_K3S_TOKEN:-${K3S_TOKEN:-}}"
ns="${SERVICE_NAMESPACE:-${NAMESPACE:-default}}"
if _prole_is_openbao_ref "$token"; then
token=$(_prole_resolve_openbao_ref "$token")
elif [[ "$token" == '${PROLE_SECRET:'* ]]; then
token=$(_prole_decrypt_prole_secret "$token")
fi
if [[ -z "$server" ]]; then
echo "DEBUG: _prole_kubeconfig_from_token: server is empty" >&2
return 1
fi
if [[ -z "$token" ]]; then
echo "DEBUG: _prole_kubeconfig_from_token: token is empty" >&2
return 1
fi
if ! _prole_token_is_bearer "$token"; then
echo "DEBUG: _prole_kubeconfig_from_token: token is not a bearer token" >&2
return 1
fi
if [[ "$server" != http* ]]; then
server="https://$server"
fi
local cfg_path
cfg_path="$(_prole_kubeconfig_path)" || return 1
# Never overwrite a kubeconfig that uses client-certificate auth
if [[ -f "$cfg_path" ]] && grep -q "client-certificate-data" "$cfg_path" 2>/dev/null; then
export KUBECONFIG="$cfg_path"
return 0
fi
mkdir -p "$(dirname "$cfg_path")"
cat >"$cfg_path" <<EOF
apiVersion: v1
kind: Config
clusters:
- name: prole-k3s
cluster:
server: ${server}
insecure-skip-tls-verify: true
users:
- name: prole-k3s
user:
token: ${token}
contexts:
- name: prole-k3s
context:
cluster: prole-k3s
user: prole-k3s
namespace: ${ns}
current-context: prole-k3s
EOF
chmod 600 "$cfg_path" 2>/dev/null || true
export KUBECONFIG="$cfg_path"
return 0
}
prole_ensure_kubeconfig() {
if [[ "${PROLE_MODE:-}" == "k3d" ]]; then
return 0
fi
local _prole_insecure_flag
_prole_insecure_flag="${PROLE_K3S_SKIP_TLS_VERIFY:-${K3S_SKIP_TLS_VERIFY:-${PROLE_K3S_INSECURE:-${K3S_INSECURE:-}}}}"
_prole_kubeconfig_maybe_insecure() {
if _prole_bool_true "${_prole_insecure_flag:-}"; then
_prole_kubeconfig_mark_insecure "${KUBECONFIG:-}" >/dev/null 2>&1 || true
fi
}
local orig_kubeconfig="${KUBECONFIG:-}"
_verify_kubeconfig() {
if [[ -n "${KUBECONFIG:-}" && -f "${KUBECONFIG:-}" ]]; then
if KUBECONFIG="${KUBECONFIG:-}" kubectl cluster-info >/dev/null 2>&1; then
return 0
fi
fi
return 1
}
# Prefer an existing valid kubeconfig (e.g. Ansible-fetched with client certs)
# over generating a new token-based one from prole.cfg.
if [[ -n "${KUBECONFIG:-}" && -f "${KUBECONFIG:-}" ]]; then
_prole_kubeconfig_maybe_insecure
if _verify_kubeconfig; then return 0; fi
fi
if [[ -n "${PROLE_SERVICE:-}" && -f "${PROLE_SERVICE}/secrets/k3s.kubeconfig" ]]; then
export KUBECONFIG="${PROLE_SERVICE}/secrets/k3s.kubeconfig"
_prole_kubeconfig_maybe_insecure
if _verify_kubeconfig; then return 0; fi
fi
if [[ -n "${PROLE_K3S_KUBECONFIG:-}" && -f "${PROLE_K3S_KUBECONFIG}" ]]; then
export KUBECONFIG="${PROLE_K3S_KUBECONFIG}"
_prole_kubeconfig_maybe_insecure
if _verify_kubeconfig; then return 0; fi
fi
if [[ -n "${PROLE_KUBECONFIG:-}" && -f "${PROLE_KUBECONFIG}" ]]; then
export KUBECONFIG="${PROLE_KUBECONFIG}"
_prole_kubeconfig_maybe_insecure
if _verify_kubeconfig; then return 0; fi
fi
if [[ -n "${PROLE_HOME:-}" && -f "${PROLE_HOME}/prole-k3s.kubeconfig" ]]; then
export KUBECONFIG="${PROLE_HOME}/prole-k3s.kubeconfig"
_prole_kubeconfig_maybe_insecure
if _verify_kubeconfig; then return 0; fi
fi
if [[ -f "${_prole_cfg_home_guess:-}/prole-k3s.kubeconfig" ]]; then
export KUBECONFIG="${_prole_cfg_home_guess:-}/prole-k3s.kubeconfig"
_prole_kubeconfig_maybe_insecure
if _verify_kubeconfig; then return 0; fi
fi
if [[ -r "/etc/rancher/k3s/k3s.yaml" ]]; then
export KUBECONFIG="/etc/rancher/k3s/k3s.yaml"
_prole_kubeconfig_maybe_insecure
if _verify_kubeconfig; then return 0; fi
fi
_prole_kubeconfig_from_token
_prole_kubeconfig_maybe_insecure
if _verify_kubeconfig; then return 0; fi
# Final fallback: if nothing worked, restore original or unset so system default is used
if [[ -n "$orig_kubeconfig" ]]; then
export KUBECONFIG="$orig_kubeconfig"
else
unset KUBECONFIG
fi
# One last check for the system default
if kubectl cluster-info >/dev/null 2>&1; then
return 0
fi
return 1
}
# Validate and switch the active kubectl context to match PROLE_MODE set in prole.cfg.
# prole.cfg always overrides the local environment.
# Call this after prole_ensure_kubeconfig in every init_*.sh script.
# Usage: prole_ensure_kube_context
prole_ensure_kube_context() {
local mode
mode="${PROLE_MODE:-}"
if [[ -z "$mode" ]]; then
return 0
fi
local current_context
current_context=$(kubectl config current-context 2>/dev/null || true)
if [[ -n "${KUBECONTEXT:-}" && "$current_context" != "$KUBECONTEXT" ]]; then
local desired_context
desired_context="$KUBECONTEXT"
local context_names
context_names=$(kubectl config get-contexts -o name 2>/dev/null || true)
local do_switch
do_switch=1
# If the requested context isn't present, try to map common shorthand to real contexts.
# For k3d, contexts are typically named `k3d-<cluster_name>`.
if ! grep -Fxq "$desired_context" <<<"$context_names"; then
if [[ "$mode" == "k3d" && "$desired_context" != k3d-* ]]; then
local prefixed
prefixed="k3d-${desired_context}"
if grep -Fxq "$prefixed" <<<"$context_names"; then
desired_context="$prefixed"
fi
fi
fi
# k3s-generated kubeconfigs often contain a single context called `default`.
# If the config only has one context, use it even when the configured KUBECONTEXT name
# doesn't match.
if ! grep -Fxq "$desired_context" <<<"$context_names"; then
local only_context=""
local context_count=0
while IFS= read -r _ctx; do
[[ -z "${_ctx:-}" ]] && continue
context_count=$((context_count + 1))
only_context="$_ctx"
done <<<"$context_names"
unset _ctx
if [[ $context_count -eq 1 && -n "${only_context:-}" ]]; then
echo "WARN: Requested kubectl context '${desired_context}' not found in active KUBECONFIG; using only available context '${only_context}'" >&2
desired_context="$only_context"
else
echo "WARN: Requested kubectl context '${desired_context}' not found in active KUBECONFIG; skipping explicit context switch and falling back to mode-based selection" >&2
do_switch=0
fi
fi
if [[ $do_switch -eq 1 ]]; then
echo "DEBUG: prole_ensure_kube_context: Switching context to '${desired_context}'" >&2
echo "INFO: Switching kubectl context to '${desired_context}' (prole.cfg overrides local env)" >&2
if kubectl config use-context "$desired_context" >/dev/null 2>&1; then
echo "INFO: kubectl context set to '${desired_context}'" >&2
return 0
else
echo "ERROR: Could not switch to context '${desired_context}'. Available contexts:" >&2
kubectl config get-contexts --no-headers 2>/dev/null | awk '{print " "$2}' >&2 || true
return 1
fi
fi
else
echo "DEBUG: prole_ensure_kube_context: current_context='${current_context}', KUBECONTEXT='${KUBECONTEXT:-<not-set>}'" >&2
fi
case "$mode" in
k3d)
# k3d contexts are always named k3d-<cluster_name>
if [[ "$current_context" == k3d-* ]]; then
return 0
fi
local cluster_name target_context
cluster_name="${K3D_CLUSTER_NAME:-knoe-dev-cluster}"
target_context="k3d-${cluster_name}"
echo "WARN: prole.cfg mode is 'k3d' but current kubectx is '${current_context:-<none>}'" >&2
echo "INFO: Switching kubectl context to '${target_context}' (prole.cfg overrides local env)" >&2
if kubectl config use-context "$target_context" >/dev/null 2>&1; then
echo "INFO: kubectl context set to '${target_context}'" >&2
else
echo "ERROR: Could not switch to context '${target_context}'. Available contexts:" >&2
kubectl config get-contexts --no-headers 2>/dev/null | awk '{print " "$2}' >&2 || true
return 1
fi
;;
k3s|k8s)
# k3s/k8s contexts must NOT be a k3d context
if [[ "$current_context" != k3d-* ]]; then
return 0
fi
echo "WARN: prole.cfg mode is '${mode}' but current kubectx is '${current_context}' (a k3d context)" >&2
echo "INFO: Switching kubectl context to match KUBECONFIG='${KUBECONFIG:-<default>}' (prole.cfg overrides local env)" >&2
local target_context
if [[ -n "${KUBECONTEXT:-}" ]]; then
target_context="${KUBECONTEXT}"
else
target_context=$(kubectl config get-contexts --no-headers 2>/dev/null \
| awk '{print $2}' | grep -v '^k3d-' | head -1 || true)
fi
if [[ -z "$target_context" ]]; then
echo "ERROR: No non-k3d context found in KUBECONFIG='${KUBECONFIG:-<default>}'. Available contexts:" >&2
kubectl config get-contexts --no-headers 2>/dev/null | awk '{print " "$2}' >&2 || true
return 1
fi
if kubectl config use-context "$target_context" >/dev/null 2>&1; then
echo "INFO: kubectl context set to '${target_context}'" >&2
else
echo "ERROR: Could not switch to context '${target_context}'" >&2
return 1
fi
;;
esac
}
if _prole_is_secret_ref "${PROLE_K3S_TOKEN:-}"; then
if _prole_is_openbao_ref "${PROLE_K3S_TOKEN:-}"; then
_decoded=$(_prole_resolve_openbao_ref "${PROLE_K3S_TOKEN:-}")
else
_decoded=$(_prole_decrypt_prole_secret "${PROLE_K3S_TOKEN:-}")
fi
if [[ -n "$_decoded" ]]; then
PROLE_K3S_TOKEN="$_decoded"
export PROLE_K3S_TOKEN
else
unset PROLE_K3S_TOKEN
fi
fi
if _prole_is_secret_ref "${K3S_TOKEN:-}"; then
if _prole_is_openbao_ref "${K3S_TOKEN:-}"; then
_decoded=$(_prole_resolve_openbao_ref "${K3S_TOKEN:-}")
else
_decoded=$(_prole_decrypt_prole_secret "${K3S_TOKEN:-}")
fi
if [[ -n "$_decoded" ]]; then
K3S_TOKEN="$_decoded"
export K3S_TOKEN
else
unset K3S_TOKEN
fi
fi
if [[ -z "${KUBECONFIG:-}" ]]; then
_prole_mode_guess=""
if command -v prole_normalize_mode >/dev/null 2>&1; then
_prole_mode_guess=$(prole_normalize_mode "${PROLE_MODE:-${DEPLOYMENT_MODE:-${CLUSTER_ENV:-}}}")
else
_prole_mode_guess="${PROLE_MODE:-${DEPLOYMENT_MODE:-${CLUSTER_ENV:-}}}"
fi
if [[ "$_prole_mode_guess" == "k3s" ]]; then
prole_ensure_kubeconfig || true
fi
unset _prole_mode_guess
fi
_prole_child_ns="${PROLE_NAMESPACE:-}"
_prole_child_id=""
if [[ -n "${PROLE_CHILD_ID:-}" ]]; then
_prole_child_id="${PROLE_CHILD_ID}"
elif [[ -n "$_prole_child_ns" && "$_prole_child_ns" =~ ^prole-db-([A-Za-z0-9]+)$ ]]; then
_prole_child_id=$(printf '%s' "${BASH_REMATCH[1]}" | tr '[:lower:]' '[:upper:]')
fi
_prole_parent_realm="${PROLE_PARENT_REALM:-${REALM:-${KRB5_REALM:-${kerberos_config_realm:-}}}}"
if [[ -n "$_prole_parent_realm" ]]; then
_prole_parent_realm=$(printf '%s' "$_prole_parent_realm" | tr '[:lower:]' '[:upper:]')
if [[ -z "${PROLE_PARENT_REALM:-}" ]]; then
PROLE_PARENT_REALM="$_prole_parent_realm"
export PROLE_PARENT_REALM
fi
fi
if [[ -n "$_prole_child_id" ]]; then
PROLE_CHILD_ID="${PROLE_CHILD_ID:-$_prole_child_id}"
export PROLE_CHILD_ID
if [[ -n "${PROLE_CHILD_REALM:-}" ]]; then
:
elif [[ -n "$_prole_parent_realm" ]]; then
if [[ "$_prole_parent_realm" == "${PROLE_CHILD_ID}."* ]]; then
PROLE_CHILD_REALM="$_prole_parent_realm"
if [[ -z "${PROLE_PARENT_REALM:-}" ]]; then
PROLE_PARENT_REALM="${_prole_parent_realm#${PROLE_CHILD_ID}.}"
export PROLE_PARENT_REALM
fi
else
PROLE_CHILD_REALM="${PROLE_CHILD_ID}.${_prole_parent_realm}"
fi
fi
if [[ -n "${PROLE_CHILD_REALM:-}" ]]; then
export PROLE_CHILD_REALM
PROLE_CHILD_WORKGROUP="${PROLE_CHILD_WORKGROUP:-${PROLE_CHILD_ID}}"
PROLE_CHILD_NETBIOS_NAME="${PROLE_CHILD_NETBIOS_NAME:-${PROLE_CHILD_ID}}"
PROLE_CHILD_SERVER_STRING="${PROLE_CHILD_SERVER_STRING:-${PROLE_CHILD_REALM} AD DC}"
export PROLE_CHILD_WORKGROUP PROLE_CHILD_NETBIOS_NAME PROLE_CHILD_SERVER_STRING
fi
fi
if [[ -z "${PROLE_USE_CHILD_REALM:-}" && -n "${PROLE_CHILD_REALM:-}" ]]; then
PROLE_USE_CHILD_REALM=1
export PROLE_USE_CHILD_REALM
fi
if _prole_bool_true "${PROLE_USE_CHILD_REALM:-}" && [[ -n "${PROLE_CHILD_REALM:-}" ]]; then
if [[ -z "${KRB5_REALM:-}" || ( -n "${PROLE_PARENT_REALM:-}" && "${KRB5_REALM}" == "${PROLE_PARENT_REALM}" ) ]]; then
KRB5_REALM="${PROLE_CHILD_REALM}"
export KRB5_REALM
fi
if [[ -z "${REALM:-}" || ( -n "${PROLE_PARENT_REALM:-}" && "${REALM}" == "${PROLE_PARENT_REALM}" ) ]]; then
REALM="${PROLE_CHILD_REALM}"
export REALM
fi
fi
prole_register_port_forward() {
local id="$1" ns="$2" target="$3" hostPort="$4" servicePort="$5"
local addr="${6:-0.0.0.0}"
local proto="${7:-TCP}"
local desc="${8:-}"
echo "PORT_FORWARD_MAPPING: id=$id;namespace=$ns;target=$target;address=$addr;hostPort=$hostPort;servicePort=$servicePort;protocol=$proto;description=$desc"
}
unset _prole_cfg_script_dir
unset _prole_cfg_home_guess
unset _prole_cfg_file
unset _PROLE_CFG_SET_VARS