#!/usr/bin/env bash # Standard include for Knoe etc scripts. # - Loads environment from env.sh (if available) # - Loads values from mode-specific config files in KNOE_CONF (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 KNOE_HOME/KNOE_CONF/etc are already set by the caller (e.g. tests), # do not allow env.sh to clobber them. _knoe_cfg_preserve_home="${KNOE_HOME:-}" _knoe_cfg_preserve_conf="${KNOE_CONF:-}" _knoe_cfg_preserve_service="${KNOE_SERVICE:-}" 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 if [[ -n "$_knoe_cfg_preserve_home" ]]; then KNOE_HOME="$_knoe_cfg_preserve_home" export KNOE_HOME fi if [[ -n "$_knoe_cfg_preserve_conf" ]]; then KNOE_CONF="$_knoe_cfg_preserve_conf" export KNOE_CONF fi if [[ -n "$_knoe_cfg_preserve_service" ]]; then KNOE_SERVICE="$_knoe_cfg_preserve_service" export KNOE_SERVICE fi unset _knoe_cfg_preserve_home _knoe_cfg_preserve_conf _knoe_cfg_preserve_service _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_normalize_key() { local key="${1:-}" key="${key//./_}" key="${key//[^A-Za-z0-9_]/_}" if [[ "$key" =~ ^[0-9] ]]; then key="_${key}" fi printf '%s' "$key" } _knoe_cfg_set_default() { local key="$1" value="$2" local token="|$key|" if [[ ! "$key" =~ ^[A-Za-z_][A-Za-z0-9_]*$ ]]; then echo "[WARN] Skipping invalid config key '$key' (not a valid shell identifier)." >&2 return 0 fi # 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 # In k3s mode the kubeconfig current-context is authoritative; never set # KUBECTL_CONTEXT / KUBE_CONTEXT_NAME / KUBECONTEXT from cfg file entries. local _knoe_read_cfg_mode _knoe_read_cfg_mode=$(knoe_normalize_mode "${DEPLOYMENT_MODE:-${KNOE_MODE:-${CLUSTER_ENV:-${knoe_mode:-}}}}" 2>/dev/null || true) 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 # Normalize to a valid shell variable key for safe export. key="$(_knoe_cfg_normalize_key "$key")" [[ -z "$key" ]] && continue # k3s: kubeconfig current-context is authoritative; skip context overrides from cfg. if [[ "$_knoe_read_cfg_mode" == "k3s" ]]; then case "$key" in KUBECTL_CONTEXT|KUBE_CONTEXT_NAME|KUBECONTEXT) continue ;; esac fi 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_named_file_for_env() { case "${1:-}" in dev) printf '%s' "k3d.cfg" ;; service|services) printf '%s' "k3s.cfg" ;; prod|production) printf '%s' "gke.cfg" ;; test|testing) printf '%s' "test.cfg" ;; *) printf '%s' "" ;; esac } _knoe_cfg_named_file_for_mode() { local mode="${1:-}" if command -v knoe_normalize_mode >/dev/null 2>&1; then mode=$(knoe_normalize_mode "$mode") fi case "$mode" in k3d) printf '%s' "k3d.cfg" ;; k3s) printf '%s' "k3s.cfg" ;; k8s) printf '%s' "gke.cfg" ;; *) printf '%s' "" ;; esac } _knoe_cfg_select_cfg_file() { local conf_dir="$1" [[ -z "$conf_dir" ]] && return 0 # If an explicit config is passed via environment, prefer it above all else. if [[ -n "${PROLE_DEPLOY_CFG:-}" && -f "${PROLE_DEPLOY_CFG}" ]]; then printf '%s' "${PROLE_DEPLOY_CFG}" return 0 fi local mode_hint mode_file env_hint env_file cfg mode_hint="${KNOE_MODE:-${DEPLOYMENT_MODE:-${CLUSTER_ENV:-}}}" mode_file="$(_knoe_cfg_named_file_for_mode "$mode_hint")" if [[ -n "$mode_file" && -f "$conf_dir/$mode_file" ]]; then printf '%s' "$conf_dir/$mode_file" return 0 fi for cfg in k3d.cfg k3s.cfg gke.cfg test.cfg; do if [[ -f "$conf_dir/$cfg" ]]; then printf '%s' "$conf_dir/$cfg" return 0 fi done env_hint="$(_knoe_cfg_env_from_hint "$mode_hint")" if [[ -n "$env_hint" ]]; then env_file="$(_knoe_cfg_named_file_for_env "$env_hint")" if [[ -n "$env_file" && -f "$conf_dir/$env_hint/$env_file" ]]; then printf '%s' "$conf_dir/$env_hint/$env_file" return 0 fi if [[ -f "$conf_dir/$env_hint/knoe.cfg" ]]; then printf '%s' "$conf_dir/$env_hint/knoe.cfg" return 0 fi fi for cfg in dev service prod test; do env_file="$(_knoe_cfg_named_file_for_env "$cfg")" if [[ -n "$env_file" && -f "$conf_dir/$cfg/$env_file" ]]; then printf '%s' "$conf_dir/$cfg/$env_file" return 0 fi if [[ -f "$conf_dir/$cfg/knoe.cfg" ]]; then printf '%s' "$conf_dir/$cfg/knoe.cfg" return 0 fi done if [[ -f "$conf_dir/knoe.cfg" ]]; then printf '%s' "$conf_dir/knoe.cfg" return 0 fi } _knoe_cfg_layer_files() { local entry="$1" [[ -z "$entry" ]] && return 0 local resolved env_dir env_name base base_name f env_base resolved=$(_knoe_cfg_realpath "$entry") env_dir=$(cd "$(dirname "$resolved")" 2>/dev/null && pwd || true) env_name=$(basename "$env_dir") base_name=$(basename "$resolved") if [[ "$base_name" =~ ^(k3d|k3s|gke|test)\.cfg$ ]]; then printf '%s\n' "$resolved" return 0 fi base="$env_dir/$base_name" if [[ "$env_name" =~ ^(dev|service|prod|test)$ ]]; then env_base="$(_knoe_cfg_named_file_for_env "$env_name")" if [[ "$base_name" == "knoe.cfg" && -n "$env_base" && -f "$env_dir/$env_base" ]]; then base="$env_dir/$env_base" elif [[ ! -f "$base" && -n "$env_base" && -f "$env_dir/$env_base" ]]; then base="$env_dir/$env_base" fi fi 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")" == "$(basename "$base")" ]] && continue [[ "$(basename "$f")" == "prod.cfg" ]] && continue # standalone GKE deploy config, not a knoe.cfg overlay [[ "$(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 fi # 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 # NO DEFAULT FALLBACK TO "dev" here. If env is unknown, we must fail or remain unknown # so that explicit contexts (like k8s) aren't clobbered by a "dev" (k3d) default. if [[ -z "$env" ]]; then if [[ "${_PROLE_CFG_STRICT:-}" == "1" ]]; then echo "ERROR: Could not resolve environment (dev/service/prod/test) from hints or config." >&2 exit 1 fi # If not strict, we don't set a default env, allowing caller to handle it. fi # If conf_dir is already the env directory, the base config lives here directly. local base local env_cfg env_cfg="$(_knoe_cfg_named_file_for_env "$env")" [[ -z "$env_cfg" ]] && env_cfg="knoe.cfg" if [[ "$is_env_dir" == true ]]; then base="$conf_dir/$env_cfg" else base="$conf_dir/$env_cfg" fi # If conf_dir is already the env dir, the entry IS the base — just ensure it exists. if [[ "$is_env_dir" == true ]]; then if [[ -L "$entry" ]]; then # Remove stale symlink so we can replace it with the real file. local target_file target_file=$(readlink "$entry" 2>/dev/null || true) local resolved="$conf_dir/$target_file" if [[ -f "$resolved" ]]; then cp "$resolved" "$entry.tmp" 2>/dev/null || true rm -f "$entry" 2>/dev/null || true mv "$entry.tmp" "$entry" 2>/dev/null || true else rm -f "$entry" 2>/dev/null || true _knoe_cfg_write_default_base "$base" "$env" fi elif [[ ! -f "$entry" ]]; then _knoe_cfg_write_default_base "$base" "$env" fi return 0 fi # Remove stale broken symlink (no replacement symlink — Python resolves env path directly). if [[ -L "$entry" && ! -e "$entry" ]]; then rm -f "$entry" 2>/dev/null || true [[ -f "$base" ]] || _knoe_cfg_write_default_base "$base" "$env" 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 # No symlink created — Python resolves env path from CLUSTER_ENV directly. return 0 fi # If entrypoint is missing, create the env-specific base (no symlink needed). if [[ ! -e "$entry" ]]; then [[ -f "$base" ]] || _knoe_cfg_write_default_base "$base" "$env" 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:-}" ]]; then _knoe_cfg_file="$(_knoe_cfg_select_cfg_file "$KNOE_CONF")" fi if [[ -z "$_knoe_cfg_file" && -n "${KNOE_HOME:-}" ]]; then _knoe_cfg_file="$(_knoe_cfg_select_cfg_file "$KNOE_HOME/conf")" fi if [[ -z "$_knoe_cfg_file" ]]; then _knoe_cfg_file="$(_knoe_cfg_select_cfg_file "$_knoe_cfg_home_guess/conf")" fi if [[ -n "$_knoe_cfg_file" ]]; then if [[ -z "${KNOE_CONF:-}" ]]; then KNOE_CONF=$(cd "$(dirname "$_knoe_cfg_file")" && pwd) export KNOE_CONF fi # Migrate legacy knoe.cfg layouts only when a legacy entrypoint is selected. if [[ "$(basename "$_knoe_cfg_file")" == "knoe.cfg" ]]; then _knoe_cfg_bootstrap_env_layout "$KNOE_CONF" _knoe_cfg_file="$(_knoe_cfg_select_cfg_file "$KNOE_CONF")" fi _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 the selected config file(s) (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 # Always prefer deployment/mode hint from selected config file(s) (single source of truth). # This prevents leaked environment values (e.g. KNOE_MODE=k3d) from forcing the wrong mode. _cfg_dm=$(_knoe_cfg_extract_key_in_files "DEPLOYMENT_MODE" "${_knoe_cfg_files[@]}") if [[ -z "$_cfg_dm" ]]; then _cfg_dm=$(_knoe_cfg_extract_key_in_files "knoe.mode" "${_knoe_cfg_files[@]}") fi if [[ -n "$_cfg_dm" ]]; then DEPLOYMENT_MODE="$_cfg_dm" export DEPLOYMENT_MODE 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_sapi=$(_knoe_cfg_extract_key_in_files "SUPABASE_API_HOSTNAME" "${_knoe_cfg_files[@]}") if [[ -z "$_cfg_sapi" ]]; then _cfg_sapi=$(_knoe_cfg_extract_key_in_files "supabase_api_hostname" "${_knoe_cfg_files[@]}") fi if [[ -n "$_cfg_sapi" ]]; then export SUPABASE_API_HOSTNAME="$_cfg_sapi" fi _cfg_sstudio=$(_knoe_cfg_extract_key_in_files "SUPABASE_STUDIO_HOSTNAME" "${_knoe_cfg_files[@]}") if [[ -z "$_cfg_sstudio" ]]; then _cfg_sstudio=$(_knoe_cfg_extract_key_in_files "supabase_studio_hostname" "${_knoe_cfg_files[@]}") fi if [[ -n "$_cfg_sstudio" ]]; then export SUPABASE_STUDIO_HOSTNAME="$_cfg_sstudio" fi _cfg_sing=$(_knoe_cfg_extract_key_in_files "SUPABASE_INGRESS_CLASS" "${_knoe_cfg_files[@]}") if [[ -z "$_cfg_sing" ]]; then _cfg_sing=$(_knoe_cfg_extract_key_in_files "supabase_ingress_class" "${_knoe_cfg_files[@]}") fi if [[ -n "$_cfg_sing" ]]; then export SUPABASE_INGRESS_CLASS="$_cfg_sing" fi _cfg_gitdom=$(_knoe_cfg_extract_key_in_files "GITLAB_DOMAIN" "${_knoe_cfg_files[@]}") if [[ -z "$_cfg_gitdom" ]]; then _cfg_gitdom=$(_knoe_cfg_extract_key_in_files "gitlab_domain" "${_knoe_cfg_files[@]}") fi if [[ -z "$_cfg_gitdom" ]]; then _cfg_gitdom=$(_knoe_cfg_extract_key_in_files "GITLAB_HOSTNAME" "${_knoe_cfg_files[@]}") fi if [[ -n "$_cfg_gitdom" ]]; then export GITLAB_DOMAIN="$_cfg_gitdom" fi _cfg_githosts=$(_knoe_cfg_extract_key_in_files "GITLAB_PUBLIC_HOSTS" "${_knoe_cfg_files[@]}") if [[ -z "$_cfg_githosts" ]]; then _cfg_githosts=$(_knoe_cfg_extract_key_in_files "gitlab_public_hosts" "${_knoe_cfg_files[@]}") fi if [[ -n "$_cfg_githosts" ]]; then export GITLAB_PUBLIC_HOSTS="$_cfg_githosts" fi _cfg_ging=$(_knoe_cfg_extract_key_in_files "GITLAB_INGRESS_CLASS" "${_knoe_cfg_files[@]}") if [[ -z "$_cfg_ging" ]]; then _cfg_ging=$(_knoe_cfg_extract_key_in_files "gitlab_ingress_class" "${_knoe_cfg_files[@]}") fi if [[ -n "$_cfg_ging" ]]; then export GITLAB_INGRESS_CLASS="$_cfg_ging" fi _cfg_auth_enabled=$(_knoe_cfg_extract_key_in_files "AUTHORITY_ENABLED" "${_knoe_cfg_files[@]}") if [[ -z "$_cfg_auth_enabled" ]]; then _cfg_auth_enabled=$(_knoe_cfg_extract_key_in_files "authority_enabled" "${_knoe_cfg_files[@]}") fi if [[ -n "$_cfg_auth_enabled" ]]; then export AUTHORITY_ENABLED="$_cfg_auth_enabled" export FRONTDOOR_AUTH_ENABLED="$_cfg_auth_enabled" fi _cfg_auth_host=$(_knoe_cfg_extract_key_in_files "AUTH_HOSTNAME" "${_knoe_cfg_files[@]}") if [[ -z "$_cfg_auth_host" ]]; then _cfg_auth_host=$(_knoe_cfg_extract_key_in_files "auth_hostname" "${_knoe_cfg_files[@]}") fi if [[ -n "$_cfg_auth_host" ]]; then export AUTH_HOSTNAME="$_cfg_auth_host" fi _cfg_auth_verify=$(_knoe_cfg_extract_key_in_files "AUTH_VERIFY_PATH" "${_knoe_cfg_files[@]}") if [[ -z "$_cfg_auth_verify" ]]; then _cfg_auth_verify=$(_knoe_cfg_extract_key_in_files "auth_verify_path" "${_knoe_cfg_files[@]}") fi if [[ -n "$_cfg_auth_verify" ]]; then export AUTH_VERIFY_PATH="$_cfg_auth_verify" fi _cfg_auth_login=$(_knoe_cfg_extract_key_in_files "AUTH_LOGIN_PATH" "${_knoe_cfg_files[@]}") if [[ -z "$_cfg_auth_login" ]]; then _cfg_auth_login=$(_knoe_cfg_extract_key_in_files "auth_login_path" "${_knoe_cfg_files[@]}") fi if [[ -n "$_cfg_auth_login" ]]; then export AUTH_LOGIN_PATH="$_cfg_auth_login" fi _cfg_auth_headers=$(_knoe_cfg_extract_key_in_files "AUTH_RESPONSE_HEADERS" "${_knoe_cfg_files[@]}") if [[ -z "$_cfg_auth_headers" ]]; then _cfg_auth_headers=$(_knoe_cfg_extract_key_in_files "auth_response_headers" "${_knoe_cfg_files[@]}") fi if [[ -n "$_cfg_auth_headers" ]]; then export AUTH_RESPONSE_HEADERS="$_cfg_auth_headers" fi _cfg_ctx=$(_knoe_cfg_extract_key_in_files "KUBE_CONTEXT_NAME" "${_knoe_cfg_files[@]}") if [[ -z "$_cfg_ctx" ]]; then _cfg_ctx=$(_knoe_cfg_extract_key_in_files "KUBECTL_CONTEXT" "${_knoe_cfg_files[@]}") fi if [[ -z "$_cfg_ctx" ]]; then _cfg_ctx=$(_knoe_cfg_extract_key_in_files "KUBECONTEXT" "${_knoe_cfg_files[@]}") fi _cfg_mode_hint=$(knoe_normalize_mode "${DEPLOYMENT_MODE:-${KNOE_MODE:-${CLUSTER_ENV:-${knoe_mode:-}}}}") _cfg_app_ctx="${APP_CLUSTER_KUBECONTEXT:-${init_cluster_app_cluster_kubecontext:-}}" if [[ "$_cfg_mode_hint" == "k8s" ]]; then if [[ -z "$_cfg_app_ctx" ]]; then _cfg_app_ctx=$(_knoe_cfg_extract_key_in_files "APP_CLUSTER_KUBECONTEXT" "${_knoe_cfg_files[@]}") fi if [[ -z "$_cfg_app_ctx" ]]; then _cfg_app_ctx=$(_knoe_cfg_extract_key_in_files "app_cluster_kubecontext" "${_knoe_cfg_files[@]}") fi if [[ -z "$_cfg_app_ctx" ]]; then _cfg_app_ctx=$(_knoe_cfg_extract_key_in_files "init_cluster.app_cluster_kubecontext" "${_knoe_cfg_files[@]}") fi if [[ -z "$_cfg_app_ctx" ]]; then _cfg_app_ctx=$(_knoe_cfg_extract_key_in_files "init_cluster_app_cluster_kubecontext" "${_knoe_cfg_files[@]}") fi if [[ -n "$_cfg_app_ctx" ]]; then _cfg_ctx="$_cfg_app_ctx" fi fi if [[ -n "$_cfg_ctx" ]]; then _runtime_ctx="${KUBECTL_CONTEXT:-${KUBE_CONTEXT_NAME:-${KUBECONTEXT:-}}}" _override_runtime_ctx=0 # Respect explicitly provided runtime context (e.g., installer per-role env), # but in k8s split-cluster mode override legacy aliases with APP context. if [[ -z "$_runtime_ctx" ]]; then _override_runtime_ctx=1 elif [[ "$_cfg_mode_hint" == "k8s" && -n "$_cfg_app_ctx" ]]; then case "$_runtime_ctx" in prod|production|knoe-prod-cluster) _override_runtime_ctx=1 ;; esac fi if [[ $_override_runtime_ctx -eq 1 && "$_cfg_mode_hint" != "k3s" ]]; then export KUBE_CONTEXT_NAME="$_cfg_ctx" export KUBECTL_CONTEXT="$_cfg_ctx" export KUBECONTEXT="$_cfg_ctx" fi unset _runtime_ctx _override_runtime_ctx fi unset _cfg_ns _cfg_sns _cfg_dm _cfg_sh _cfg_sbh _cfg_sapi _cfg_sstudio _cfg_sing _cfg_gitdom _cfg_githosts _cfg_ging _cfg_auth_enabled _cfg_auth_host _cfg_auth_verify _cfg_auth_login _cfg_auth_headers _cfg_ctx _cfg_mode_hint _cfg_app_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 "${KNOE_IMAGE_REGISTRY:-}" ]]; then LOCAL_REGISTRY_INTERNAL="${KNOE_IMAGE_REGISTRY}" export LOCAL_REGISTRY_INTERNAL elif [[ -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 "${KNOE_IMAGE_REGISTRY:-}" ]]; then LOCAL_REGISTRY_INTERNAL="${KNOE_IMAGE_REGISTRY}" export LOCAL_REGISTRY_INTERNAL elif [[ -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 } # Resolve the effective kubeconfig path used by kubectl. # If KUBECONFIG contains multiple files, use the first file for mutation checks. resolve_kubeconfig_path() { local kubeconfig_raw="${KUBECONFIG:-}" if [[ -n "$kubeconfig_raw" ]]; then printf '%s' "${kubeconfig_raw%%:*}" return 0 fi if [[ -n "${HOME:-}" && -f "${HOME}/.kube/config" ]]; then printf '%s' "${HOME}/.kube/config" return 0 fi printf '%s' "" return 0 } resolve_kube_context_name() { if [[ -n "${KUBE_CONTEXT_NAME:-}" ]]; then printf '%s' "${KUBE_CONTEXT_NAME}" return 0 fi if [[ -n "${KUBECTL_CONTEXT:-}" ]]; then printf '%s' "${KUBECTL_CONTEXT}" return 0 fi printf '%s' "${KUBECONTEXT:-}" return 0 } kubeconfig_path_is_writable() { local kubeconfig_path="$1" if [[ -z "$kubeconfig_path" ]]; then return 0 fi if [[ -e "$kubeconfig_path" ]]; then [[ -w "$kubeconfig_path" ]] return $? fi local parent_dir parent_dir=$(dirname "$kubeconfig_path") [[ -w "$parent_dir" ]] } detect_local_cluster_node() { local current_context="${1:-}" local cluster_server="${2:-}" local kubeconfig_path="${3:-}" if [[ -z "$current_context" ]]; then current_context=$(kubectl config current-context 2>/dev/null || true) fi if [[ -z "$cluster_server" ]]; then cluster_server=$(kubectl config view --minify -o jsonpath='{.clusters[0].cluster.server}' 2>/dev/null || true) fi if [[ -z "$kubeconfig_path" ]]; then kubeconfig_path=$(resolve_kubeconfig_path) fi if [[ "$kubeconfig_path" == "/etc/rancher/k3s/k3s.yaml" && "$current_context" == "default" ]]; then case "$cluster_server" in https://127.0.0.1:6443|https://localhost:6443) return 0 ;; esac fi return 1 } switch_kube_context() { local target_context="$1" local switch_reason="$2" local kubeconfig_path="$3" if [[ -z "$target_context" ]]; then echo "ERROR: Refusing empty kubectl context switch target" >&2 return 1 fi if ! kubeconfig_path_is_writable "$kubeconfig_path"; then echo "ERROR: kubeconfig '${kubeconfig_path:-}' is not writable; context switch to '${target_context}' is required (${switch_reason})." >&2 return 1 fi echo "INFO: Writable remote kubeconfig mode; switching kubectl context to '${target_context}' (${switch_reason})" >&2 if kubectl config use-context "$target_context" >/dev/null 2>&1; then echo "INFO: kubectl context set to '${target_context}'" >&2 return 0 fi 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 } _knoe_list_kubeconfig_candidates() { local -a candidates if [[ -n "${KUBECONFIG:-}" ]]; then IFS=':' read -r -a _kcfg_parts <<<"${KUBECONFIG}" for _kcfg in "${_kcfg_parts[@]}"; do [[ -n "${_kcfg:-}" ]] && candidates+=("$_kcfg") done unset _kcfg_parts _kcfg fi [[ -n "${PROLE_K3S_KUBECONFIG:-}" ]] && candidates+=("${PROLE_K3S_KUBECONFIG}") [[ -n "${PROLE_KUBECONFIG:-}" ]] && candidates+=("${PROLE_KUBECONFIG}") [[ -n "${KNOE_HOME:-}" ]] && candidates+=("${KNOE_HOME}/knoe-k3s.kubeconfig") [[ -n "${_knoe_cfg_home_guess:-}" ]] && candidates+=("${_knoe_cfg_home_guess}/knoe-k3s.kubeconfig") [[ -n "${HOME:-}" ]] && candidates+=("${HOME}/.kube/config") [[ -n "${KNOE_SERVICE:-}" ]] && candidates+=("${KNOE_SERVICE}/secrets/k3s.kubeconfig") candidates+=("/etc/rancher/k3s/k3s.yaml") local -A seen local candidate for candidate in "${candidates[@]}"; do [[ -z "${candidate:-}" ]] && continue [[ -f "$candidate" ]] || continue [[ -n "${seen[$candidate]+x}" ]] && continue seen[$candidate]=1 printf '%s\n' "$candidate" done } _knoe_select_kubeconfig_for_context() { local desired_context="$1" [[ -n "$desired_context" ]] || return 1 local candidate contexts while IFS= read -r candidate; do [[ -n "${candidate:-}" ]] || continue contexts=$(KUBECONFIG="$candidate" kubectl config get-contexts -o name 2>/dev/null || true) if grep -Fxq "$desired_context" <<<"$contexts"; then export KUBECONFIG="$candidate" echo "INFO: Selected kubeconfig '$candidate' for context '${desired_context}'" >&2 return 0 fi done < <(_knoe_list_kubeconfig_candidates) 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: ensure_kube_context ensure_kube_context() { local mode mode="${KNOE_MODE:-}" if [[ -z "$mode" ]]; then return 0 fi local desired_context desired_context=$(resolve_kube_context_name) if [[ -n "$desired_context" ]]; then export KUBE_CONTEXT_NAME="$desired_context" export KUBECONTEXT="$desired_context" fi local current_context current_context=$(kubectl config current-context 2>/dev/null || true) local cluster_server cluster_server=$(kubectl config view --minify -o jsonpath='{.clusters[0].cluster.server}' 2>/dev/null || true) local kubeconfig_path kubeconfig_path=$(resolve_kubeconfig_path) if detect_local_cluster_node "$current_context" "$cluster_server" "$kubeconfig_path"; then echo "INFO: Detected local cluster-node kubeconfig; current-context=${current_context:-}, server=${cluster_server:-}; skipping context mutation" >&2 return 0 fi if [[ -n "$desired_context" && "$current_context" != "$desired_context" ]]; then echo "DEBUG: Remote kubeconfig mode; current-context='${current_context:-}', desired-context='${desired_context}', kubeconfig='${kubeconfig_path:-}'" >&2 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:-}" && "$only_context" != "default" ]]; then echo "WARN: Requested kubectl context '${desired_context}' not found in active KUBECONFIG; using only available context '${only_context}'" >&2 desired_context="$only_context" elif [[ $context_count -eq 1 && "${only_context:-}" == "default" ]]; then if _knoe_select_kubeconfig_for_context "$desired_context"; then context_names=$(kubectl config get-contexts -o name 2>/dev/null || true) if grep -Fxq "$desired_context" <<<"$context_names"; then only_context="" 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 fi else echo "ERROR: Requested kubectl context '${desired_context}' not found in active KUBECONFIG." >&2 echo " The only available context is 'default' which is not valid for knoe operations." >&2 echo " Ensure KUBECONFIG includes a kubeconfig with context '${desired_context}'." >&2 echo " Available kubeconfigs: KUBECONFIG='${KUBECONFIG:-}'" >&2 echo " Run: export KUBECONFIG=~/.kube/config (or the file containing '${desired_context}')" >&2 return 1 fi 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 if ! switch_kube_context "$desired_context" "configured kube context override" "$kubeconfig_path"; then return 1 fi return 0 fi else echo "DEBUG: ensure_kube_context: current_context='${current_context}', KUBE_CONTEXT_NAME='${desired_context:-}', kubeconfig='${kubeconfig_path:-}'" >&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 if ! switch_kube_context "$target_context" "k3d mode requires k3d context" "$kubeconfig_path"; then 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 ! switch_kube_context "$target_context" "k3s/k8s mode requires non-k3d context" "$kubeconfig_path"; then return 1 fi ;; esac } # Backward compatibility for existing scripts/tests that still call the legacy name. knoe_ensure_kube_context() { ensure_kube_context "$@" } 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