#!/usr/bin/env bash # Standard include for Knoe etc scripts. # - Loads environment from env.sh (if available) # - Loads values from KNOE_CONF/knoe.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 _knoe_cfg_script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) _knoe_cfg_home_guess=$(cd "$_knoe_cfg_script_dir/.." && pwd) # 1. Try to load env.sh to establish base KNOE_HOME/KNOE_CONF if [[ -n "${KNOE_HOME:-}" && -f "$KNOE_HOME/env.sh" ]]; then # shellcheck disable=SC1090 source "$KNOE_HOME/env.sh" elif [[ -f "$HOME/.knoe/env.sh" ]]; then # shellcheck disable=SC1090 source "$HOME/.knoe/env.sh" elif [[ -f "$_knoe_cfg_home_guess/env.sh" ]]; then # shellcheck disable=SC1090 source "$_knoe_cfg_home_guess/env.sh" fi _knoe_trim() { local s="$1" s="${s#"${s%%[![:space:]]*}"}" s="${s%"${s##*[![:space:]]}"}" printf '%s' "$s" } _knoe_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:'*|'${KNOE_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" } _knoe_host_from_url() { local val="${1:-}" val="${val#http://}" val="${val#https://}" val="${val%%/*}" val="${val%%:*}" printf '%s' "$val" } knoe_is_in_cluster() { [[ -n "${KUBERNETES_SERVICE_HOST:-}" && -f /var/run/secrets/kubernetes.io/serviceaccount/token ]] } _knoe_local_registry_enabled() { local raw="${PROLE_ENABLE_LOCAL_REGISTRY:-${ENABLE_LOCAL_REGISTRY:-}}" if [[ -n "$raw" ]]; then _knoe_bool_true "$raw" && return 0 return 1 fi local mode="" if command -v knoe_normalize_mode >/dev/null 2>&1; then mode=$(knoe_normalize_mode "${KNOE_MODE:-${DEPLOYMENT_MODE:-${CLUSTER_ENV:-}}}") else mode="${KNOE_MODE:-${DEPLOYMENT_MODE:-${CLUSTER_ENV:-}}}" fi [[ "$mode" == "k3d" || "$mode" == "k3s" ]] } _knoe_bool_true() { case "${1:-}" in 1|true|TRUE|True|yes|YES|Yes|on|ON|On) return 0 ;; esac return 1 } _knoe_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="|" _knoe_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 } _knoe_read_cfg() { local cfg="$1" line key value while IFS= read -r line || [[ -n "$line" ]]; do line="$(_knoe_trim "$line")" [[ -z "$line" ]] && continue case "$line" in \#*|\;*|\[*\]) continue ;; esac if [[ "$line" == *"="* ]]; then key="$(_knoe_trim "${line%%=*}")" value="$(_knoe_trim "${line#*=}")" [[ -z "$key" ]] && continue # Replace dots with underscores for shell compatibility key="${key//./_}" value="$(_knoe_cfg_expand "$value")" _knoe_cfg_set_default "$key" "$value" fi done <"$cfg" } _knoe_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="$(_knoe_cfg_expand "$value")" printf '%s' "$value" } _knoe_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="$(_knoe_cfg_expand "$value")" printf '%s' "$value" } _knoe_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" } _knoe_cfg_layer_files() { local entry="$1" [[ -z "$entry" ]] && return 0 local resolved env_dir env_name base f resolved=$(_knoe_cfg_realpath "$entry") env_dir=$(cd "$(dirname "$resolved")" 2>/dev/null && pwd || true) env_name=$(basename "$env_dir") base="$env_dir/knoe.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")" == "knoe.cfg" ]] && continue [[ "$(basename "$f")" == .* ]] && continue printf '%s\n' "$f" done return 0 fi printf '%s\n' "$entry" } _knoe_cfg_env_from_hint() { local s s=$(printf '%s' "${1:-}" | tr 'A-Z' 'a-z') case "$s" in dev|k3d|k3d-*|knoe-dev-*|knoe-dev-cluster|k3d-knoe-*) printf '%s' "dev"; return 0 ;; service|k3s|k3s-*|knoe-service-*|knoe-system|k3d-knoe-system) printf '%s' "service"; return 0 ;; prod|production|k8s|k8s-*|knoe-prod-*) printf '%s' "prod"; return 0 ;; test|testing) printf '%s' "test"; return 0 ;; esac printf '%s' "" } _knoe_cfg_default_ns_for_env() { case "${1:-}" in test) printf '%s' "knoe-test" ;; *) printf '%s' "knoe-db" ;; esac } _knoe_cfg_default_sns_for_env() { case "${1:-}" in dev) printf '%s' "default" ;; test) printf '%s' "knoe-test" ;; *) printf '%s' "knoe-system" ;; esac } _knoe_cfg_write_default_base() { local base="$1" env="$2" local ns sns ns="$(_knoe_cfg_default_ns_for_env "$env")" sns="$(_knoe_cfg_default_sns_for_env "$env")" mkdir -p "$(dirname "$base")" 2>/dev/null || true cat >"$base" </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="$(_knoe_cfg_extract_key "$entry" "DEPLOYMENT_MODE")" env="$(_knoe_cfg_env_from_hint "$env_hint")" if [[ -z "$env" ]]; then env_hint="$(_knoe_cfg_extract_key "$entry" "CLUSTER_ENV")" env="$(_knoe_cfg_env_from_hint "$env_hint")" fi if [[ -z "$env" ]]; then env_hint="$(_knoe_cfg_extract_key "$entry" "ENVIRONMENT")" env="$(_knoe_cfg_env_from_hint "$env_hint")" fi if [[ -z "$env" ]]; then # Legacy key used by older configs. env_hint="$(_knoe_cfg_extract_key "$entry" "knoe.mode")" env="$(_knoe_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="${KNOE_MODE:-${DEPLOYMENT_MODE:-${CLUSTER_ENV:-}}}" if command -v knoe_normalize_mode >/dev/null 2>&1; then env_hint=$(knoe_normalize_mode "$env_hint") fi env="$(_knoe_cfg_env_from_hint "$env_hint")" fi [[ -z "$env" ]] && env="dev" local base="$conf_dir/$env/knoe.cfg" # Heal broken symlink. if [[ -L "$entry" && ! -e "$entry" ]]; then [[ -f "$base" ]] || _knoe_cfg_write_default_base "$base" "$env" ln -snf "$env/knoe.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 knoe_conf" "$base" 2>/dev/null || grep -q "Generated by knoe_cfg.sh" "$base" 2>/dev/null; then cp "$entry" "$base" 2>/dev/null || true fi local ts legacy ts=$(date +%s) legacy="$conf_dir/knoe.cfg.legacy.${ts}" mv "$entry" "$legacy" 2>/dev/null || true ln -snf "$env/knoe.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" ]] || _knoe_cfg_write_default_base "$base" "$env" ln -snf "$env/knoe.cfg" "$entry" 2>/dev/null || true fi } knoe_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 ;; knoe-dev-cluster|k3d-knoe-dev-cluster) echo "k3d"; return 0 ;; knoe-dev-cluster|k3d-knoe-dev-cluster) echo "k3d"; return 0 ;; knoe-service-cluster) echo "k3s"; return 0 ;; knoe-prod-cluster) echo "k8s"; return 0 ;; esac echo "$s" } knoe_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=$(knoe_normalize_mode "$raw") case "$norm" in k3d|k3s|k8s) KNOE_MODE="$norm" export KNOE_MODE ;; *) echo "ERROR: Unsupported mode '$raw' (use k3d, k3s, or k8s)" >&2 exit 2 ;; esac } knoe_render_manifest() { local src="$1" if [[ "${KNOE_MODE:-}" != "k3d" ]]; then cat "$src" return 0 fi local renderer="" if [[ -n "${KNOE_SERVICE:-}" && -f "$KNOE_SERVICE/render_manifest.py" ]]; then renderer="$KNOE_SERVICE/render_manifest.py" elif [[ -n "${KNOE_HOME:-}" && -f "$KNOE_HOME/etc/render_manifest.py" ]]; then renderer="$KNOE_HOME/etc/render_manifest.py" fi if [[ -n "$renderer" ]]; then python3 "$renderer" "$src" return $? fi cat "$src" } _knoe_cfg_file="" if [[ -n "${KNOE_CONF:-}" && -f "$KNOE_CONF/knoe.cfg" ]]; then _knoe_cfg_file="$KNOE_CONF/knoe.cfg" elif [[ -n "${KNOE_HOME:-}" && -f "$KNOE_HOME/conf/knoe.cfg" ]]; then _knoe_cfg_file="$KNOE_HOME/conf/knoe.cfg" elif [[ -f "$_knoe_cfg_home_guess/conf/knoe.cfg" ]]; then _knoe_cfg_file="$_knoe_cfg_home_guess/conf/knoe.cfg" fi if [[ -n "$_knoe_cfg_file" ]]; then if [[ -z "${KNOE_CONF:-}" ]]; then KNOE_CONF=$(cd "$(dirname "$_knoe_cfg_file")" && pwd) export KNOE_CONF fi # Ensure env-dir layout exists and a stable entrypoint symlink is active. _knoe_cfg_bootstrap_env_layout "$KNOE_CONF" _knoe_cfg_file="$KNOE_CONF/knoe.cfg" _knoe_cfg_files=() while IFS= read -r _knoe_cfg_f || [[ -n "$_knoe_cfg_f" ]]; do [[ -n "$_knoe_cfg_f" ]] || continue _knoe_cfg_files+=("$_knoe_cfg_f") done < <(_knoe_cfg_layer_files "$_knoe_cfg_file") if [[ ${#_knoe_cfg_files[@]} -eq 0 ]]; then _knoe_cfg_files=("$_knoe_cfg_file") fi for _knoe_cfg_f in "${_knoe_cfg_files[@]}"; do [[ -f "$_knoe_cfg_f" ]] || continue _knoe_read_cfg "$_knoe_cfg_f" done if [[ -z "${KERBEROS_ENABLED:-}" && -n "${ENABLED:-}" ]]; then KERBEROS_ENABLED="$ENABLED" export KERBEROS_ENABLED fi # Always prefer namespace from knoe.cfg (single source of truth). _cfg_ns=$(_knoe_cfg_extract_key_in_files "NAMESPACE" "${_knoe_cfg_files[@]}") if [[ -n "$_cfg_ns" ]]; then export PROLE_NAMESPACE="$_cfg_ns" fi _cfg_sns=$(_knoe_cfg_extract_key_in_files "SERVICE_NAMESPACE" "${_knoe_cfg_files[@]}") if [[ -n "$_cfg_sns" ]]; then export SERVICE_NAMESPACE="$_cfg_sns" fi _cfg_sh=$(_knoe_cfg_extract_key_in_files "SERVICE_HOSTNAME" "${_knoe_cfg_files[@]}") if [[ -z "$_cfg_sh" ]]; then _cfg_sh=$(_knoe_cfg_extract_key_in_files "service_hostname" "${_knoe_cfg_files[@]}") fi if [[ -n "$_cfg_sh" ]]; then export SERVICE_HOSTNAME="$_cfg_sh" fi # Supabase front-door hostname (canonical external entrypoint). _cfg_sbh=$(_knoe_cfg_extract_key_in_files "supabase_hostname" "${_knoe_cfg_files[@]}") if [[ -z "$_cfg_sbh" ]]; then _cfg_sbh=$(_knoe_cfg_extract_key_in_files "SUPABASE_HOSTNAME" "${_knoe_cfg_files[@]}") fi if [[ -n "$_cfg_sbh" ]]; then export supabase_hostname="$_cfg_sbh" export SUPABASE_HOSTNAME="$_cfg_sbh" fi _cfg_ctx=$(_knoe_cfg_extract_key_in_files "KUBECONTEXT" "${_knoe_cfg_files[@]}") if [[ -n "$_cfg_ctx" ]]; then export KUBECONTEXT="$_cfg_ctx" fi unset _cfg_ns _cfg_sns _cfg_sh _cfg_sbh _cfg_ctx _knoe_cfg_files _knoe_cfg_f fi if [[ -z "${KNOE_HOME:-}" && -d "$_knoe_cfg_home_guess" ]]; then KNOE_HOME="$_knoe_cfg_home_guess" export KNOE_HOME fi # Final check for critical variables if [[ -z "${KNOE_SERVICE:-}" && -n "${KNOE_HOME:-}" ]]; then export KNOE_SERVICE="$KNOE_HOME/etc" fi # PROLE_NAMESPACE must be set from knoe.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 # Safety: knoe.cfg must override any pre-set KNOE_MODE from the environment. # Otherwise a leaked KNOE_MODE=k3d can trigger k3d-only manifest stripping (e.g., removing # `storageClassName: synology-iscsi`) while targeting a k3s cluster. _knoe_cfg_mode_hint="" if [[ -n "${knoe_mode:-}" ]]; then _knoe_cfg_mode_hint=$(knoe_normalize_mode "$knoe_mode") elif [[ -n "${DEPLOYMENT_MODE:-}" ]]; then _knoe_cfg_mode_hint=$(knoe_normalize_mode "$DEPLOYMENT_MODE") elif [[ -n "${CLUSTER_ENV:-}" ]]; then _knoe_cfg_mode_hint=$(knoe_normalize_mode "$CLUSTER_ENV") fi case "${_knoe_cfg_mode_hint}" in k3d|k3s|k8s) if [[ -n "${KNOE_MODE:-}" ]]; then _knoe_env_mode=$(knoe_normalize_mode "$KNOE_MODE") if [[ "${_knoe_env_mode}" != "${_knoe_cfg_mode_hint}" ]]; then echo "WARN: Overriding KNOE_MODE='${_knoe_env_mode}' with knoe.cfg mode '${_knoe_cfg_mode_hint}'." >&2 KNOE_MODE="${_knoe_cfg_mode_hint}" export KNOE_MODE fi unset _knoe_env_mode fi ;; esac unset _knoe_cfg_mode_hint if [[ -z "${KNOE_MODE:-}" ]]; then if [[ -n "${knoe_mode:-}" ]]; then _knoe_mode_guess=$(knoe_normalize_mode "$knoe_mode") case "$_knoe_mode_guess" in k3d|k3s|k8s) KNOE_MODE="$_knoe_mode_guess"; export KNOE_MODE ;; esac unset _knoe_mode_guess elif [[ -n "${DEPLOYMENT_MODE:-}" ]]; then _knoe_mode_guess=$(knoe_normalize_mode "$DEPLOYMENT_MODE") case "$_knoe_mode_guess" in k3d|k3s|k8s) KNOE_MODE="$_knoe_mode_guess"; export KNOE_MODE ;; esac unset _knoe_mode_guess elif [[ -n "${CLUSTER_ENV:-}" ]]; then _knoe_mode_guess=$(knoe_normalize_mode "$CLUSTER_ENV") case "$_knoe_mode_guess" in k3d|k3s|k8s) KNOE_MODE="$_knoe_mode_guess"; export KNOE_MODE ;; esac unset _knoe_mode_guess fi fi if [[ -n "${PROLE_NAMESPACE:-}" ]]; then _knoe_mode_resolved=$(knoe_normalize_mode "${KNOE_MODE:-}") case "$_knoe_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 _knoe_local_registry_enabled; then LOCAL_REGISTRY_INTERNAL="k3d-knoe-registry:5000" export LOCAL_REGISTRY_INTERNAL fi ;; esac case "$_knoe_mode_resolved" in k3s|k8s) # Registry namespace is driven by config (SERVICE_NAMESPACE) unless explicitly overridden. _knoe_registry_ns="${REGISTRY_NAMESPACE:-${SERVICE_NAMESPACE:-${PROLE_NAMESPACE:-}}}" if [[ -n "${LOCAL_REGISTRY_INTERNAL:-}" ]]; then # Values like ".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 _knoe_local_registry_enabled; then _knoe_registry_host="$(_knoe_host_from_url "${PROLE_K3S_SERVER:-${K3S_SERVER_URL:-}}")" if [[ -n "${_knoe_registry_host:-}" ]]; then LOCAL_REGISTRY_INTERNAL="${_knoe_registry_host}:5000" export LOCAL_REGISTRY_INTERNAL elif [[ -n "${_knoe_registry_ns:-}" ]]; then LOCAL_REGISTRY_INTERNAL="registry.${_knoe_registry_ns}.svc.cluster.local:5000" export LOCAL_REGISTRY_INTERNAL fi unset _knoe_registry_host fi unset _knoe_registry_ns ;; esac unset _knoe_mode_resolved fi _knoe_is_secret_ref() { case "${1:-}" in '${KNOE_SECRET:'*|'${OPENBAO:'*) return 0 ;; esac return 1 } _knoe_is_openbao_ref() { case "${1:-}" in '${OPENBAO:'*) return 0 ;; esac return 1 } _knoe_decrypt_knoe_secret() { local value="${1:-}" case "$value" in '${KNOE_SECRET:'*) ;; *) printf '%s' "$value"; return 0 ;; esac python3 - "$value" <<'PY' import base64 import getpass import platform import subprocess import sys from pathlib import Path KNOE_SECRET_PREFIX = "${KNOE_SECRET:" KNOE_SECRET_SUFFIX = "}" KNOE_SECRET_VERSION = "v1" KNOE_SECRET_SERVICE = "knoe-installer" KNOE_SECRET_KEY_FILE = Path.home() / ".knoe" / "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 "knoe" if system == "Darwin": return get_keychain_key(KNOE_SECRET_SERVICE, account) return get_file_key(KNOE_SECRET_KEY_FILE) def decrypt_knoe_secret(value: str) -> str: if not (value.startswith(KNOE_SECRET_PREFIX) and value.endswith(KNOE_SECRET_SUFFIX)): return value inner = value[len(KNOE_SECRET_PREFIX):-len(KNOE_SECRET_SUFFIX)] parts = inner.split(":") if len(parts) != 3 or parts[0] != KNOE_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_knoe_secret(value) print(out) PY } _knoe_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: knoe_service = os.environ.get("KNOE_SERVICE", "") if knoe_service: token_path = os.path.join(knoe_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("KNOE_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("KNOE_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 } _knoe_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 } _knoe_kubeconfig_path() { local base="" # Try KNOE_SERVICE, then KNOE_HOME, then guess local candidates=("${KNOE_SERVICE:-}/secrets" "${KNOE_HOME:-}" "${_knoe_cfg_home_guess:-}") for b in "${candidates[@]}"; do if [[ -z "$b" ]]; then continue; fi if _knoe_usable_dir "$b" >/dev/null; then base="$b" break fi done # Fallback to ~/.knoe if everything else failed if [[ -z "$base" ]]; then base="$HOME/.knoe/secrets" mkdir -p "$base" 2>/dev/null || true fi if [[ "$base" == */secrets ]]; then printf '%s\n' "${base}/k3s.kubeconfig" else printf '%s\n' "${base}/knoe-k3s.kubeconfig" fi } _knoe_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 } _knoe_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 _knoe_is_openbao_ref "$token"; then token=$(_knoe_resolve_openbao_ref "$token") elif [[ "$token" == '${KNOE_SECRET:'* ]]; then token=$(_knoe_decrypt_knoe_secret "$token") fi if [[ -z "$server" ]]; then echo "DEBUG: _knoe_kubeconfig_from_token: server is empty" >&2 return 1 fi if [[ -z "$token" ]]; then echo "DEBUG: _knoe_kubeconfig_from_token: token is empty" >&2 return 1 fi if ! _knoe_token_is_bearer "$token"; then echo "DEBUG: _knoe_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="$(_knoe_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" </dev/null || true export KUBECONFIG="$cfg_path" return 0 } knoe_ensure_kubeconfig() { if [[ "${KNOE_MODE:-}" == "k3d" ]]; then return 0 fi local _knoe_insecure_flag _knoe_insecure_flag="${PROLE_K3S_SKIP_TLS_VERIFY:-${K3S_SKIP_TLS_VERIFY:-${PROLE_K3S_INSECURE:-${K3S_INSECURE:-}}}}" _knoe_kubeconfig_maybe_insecure() { if _knoe_bool_true "${_knoe_insecure_flag:-}"; then _knoe_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 knoe.cfg. if [[ -n "${KUBECONFIG:-}" && -f "${KUBECONFIG:-}" ]]; then _knoe_kubeconfig_maybe_insecure if _verify_kubeconfig; then return 0; fi fi if [[ -n "${KNOE_SERVICE:-}" && -f "${KNOE_SERVICE}/secrets/k3s.kubeconfig" ]]; then export KUBECONFIG="${KNOE_SERVICE}/secrets/k3s.kubeconfig" _knoe_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}" _knoe_kubeconfig_maybe_insecure if _verify_kubeconfig; then return 0; fi fi if [[ -n "${PROLE_KUBECONFIG:-}" && -f "${PROLE_KUBECONFIG}" ]]; then export KUBECONFIG="${PROLE_KUBECONFIG}" _knoe_kubeconfig_maybe_insecure if _verify_kubeconfig; then return 0; fi fi if [[ -n "${KNOE_HOME:-}" && -f "${KNOE_HOME}/knoe-k3s.kubeconfig" ]]; then export KUBECONFIG="${KNOE_HOME}/knoe-k3s.kubeconfig" _knoe_kubeconfig_maybe_insecure if _verify_kubeconfig; then return 0; fi fi if [[ -f "${_knoe_cfg_home_guess:-}/knoe-k3s.kubeconfig" ]]; then export KUBECONFIG="${_knoe_cfg_home_guess:-}/knoe-k3s.kubeconfig" _knoe_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" _knoe_kubeconfig_maybe_insecure if _verify_kubeconfig; then return 0; fi fi _knoe_kubeconfig_from_token _knoe_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 KNOE_MODE set in knoe.cfg. # knoe.cfg always overrides the local environment. # Call this after knoe_ensure_kubeconfig in every init_*.sh script. # Usage: knoe_ensure_kube_context knoe_ensure_kube_context() { local mode mode="${KNOE_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-`. 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: knoe_ensure_kube_context: Switching context to '${desired_context}'" >&2 echo "INFO: Switching kubectl context to '${desired_context}' (knoe.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: knoe_ensure_kube_context: current_context='${current_context}', KUBECONTEXT='${KUBECONTEXT:-}'" >&2 fi case "$mode" in k3d) # k3d contexts are always named k3d- 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: knoe.cfg mode is 'k3d' but current kubectx is '${current_context:-}'" >&2 echo "INFO: Switching kubectl context to '${target_context}' (knoe.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: knoe.cfg mode is '${mode}' but current kubectx is '${current_context}' (a k3d context)" >&2 echo "INFO: Switching kubectl context to match KUBECONFIG='${KUBECONFIG:-}' (knoe.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:-}'. 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 _knoe_is_secret_ref "${PROLE_K3S_TOKEN:-}"; then if _knoe_is_openbao_ref "${PROLE_K3S_TOKEN:-}"; then _decoded=$(_knoe_resolve_openbao_ref "${PROLE_K3S_TOKEN:-}") else _decoded=$(_knoe_decrypt_knoe_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 _knoe_is_secret_ref "${K3S_TOKEN:-}"; then if _knoe_is_openbao_ref "${K3S_TOKEN:-}"; then _decoded=$(_knoe_resolve_openbao_ref "${K3S_TOKEN:-}") else _decoded=$(_knoe_decrypt_knoe_secret "${K3S_TOKEN:-}") fi if [[ -n "$_decoded" ]]; then K3S_TOKEN="$_decoded" export K3S_TOKEN else unset K3S_TOKEN fi fi if [[ -z "${KUBECONFIG:-}" ]]; then _knoe_mode_guess="" if command -v knoe_normalize_mode >/dev/null 2>&1; then _knoe_mode_guess=$(knoe_normalize_mode "${KNOE_MODE:-${DEPLOYMENT_MODE:-${CLUSTER_ENV:-}}}") else _knoe_mode_guess="${KNOE_MODE:-${DEPLOYMENT_MODE:-${CLUSTER_ENV:-}}}" fi if [[ "$_knoe_mode_guess" == "k3s" ]]; then knoe_ensure_kubeconfig || true fi unset _knoe_mode_guess fi _knoe_child_ns="${PROLE_NAMESPACE:-}" _knoe_child_id="" if [[ -n "${PROLE_CHILD_ID:-}" ]]; then _knoe_child_id="${PROLE_CHILD_ID}" elif [[ -n "$_knoe_child_ns" && "$_knoe_child_ns" =~ ^knoe-db-([A-Za-z0-9]+)$ ]]; then _knoe_child_id=$(printf '%s' "${BASH_REMATCH[1]}" | tr '[:lower:]' '[:upper:]') fi _knoe_parent_realm="${PROLE_PARENT_REALM:-${REALM:-${KRB5_REALM:-${kerberos_config_realm:-}}}}" if [[ -n "$_knoe_parent_realm" ]]; then _knoe_parent_realm=$(printf '%s' "$_knoe_parent_realm" | tr '[:lower:]' '[:upper:]') if [[ -z "${PROLE_PARENT_REALM:-}" ]]; then PROLE_PARENT_REALM="$_knoe_parent_realm" export PROLE_PARENT_REALM fi fi if [[ -n "$_knoe_child_id" ]]; then PROLE_CHILD_ID="${PROLE_CHILD_ID:-$_knoe_child_id}" export PROLE_CHILD_ID if [[ -n "${PROLE_CHILD_REALM:-}" ]]; then : elif [[ -n "$_knoe_parent_realm" ]]; then if [[ "$_knoe_parent_realm" == "${PROLE_CHILD_ID}."* ]]; then PROLE_CHILD_REALM="$_knoe_parent_realm" if [[ -z "${PROLE_PARENT_REALM:-}" ]]; then PROLE_PARENT_REALM="${_knoe_parent_realm#${PROLE_CHILD_ID}.}" export PROLE_PARENT_REALM fi else PROLE_CHILD_REALM="${PROLE_CHILD_ID}.${_knoe_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 _knoe_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 knoe_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 _knoe_cfg_script_dir unset _knoe_cfg_home_guess unset _knoe_cfg_file unset _PROLE_CFG_SET_VARS