mirror of
https://github.com/dredx/prole.git
synced 2026-09-23 12:03:59 +00:00
Implement split-cluster host ownership and context-safe deploy wiring
- move Supabase k8s ingress defaults to env-indexed api/db hostnames and remove legacy host bleed-through - enforce explicit APP/DB kubecontext role validation across cluster ops and init scripts - align env/default derivation and extend tests for hostname rendering and context checks Co-authored-by: Junie <junie@jetbrains.com>
This commit is contained in:
parent
be85d2e4b2
commit
bfa712273e
10
conf/gke.cfg
10
conf/gke.cfg
@ -117,19 +117,19 @@ PROLE_HOME = $HOME/dev/prole
|
||||
REGISTRY_NAMESPACE = knoe-system
|
||||
SERVICE_NAMESPACE = knoe-system
|
||||
SUPABASE_HOSTNAME = db.0.knoe.dev
|
||||
SUPABASE_API_HOSTNAME = api.knoe.dev
|
||||
SUPABASE_STUDIO_HOSTNAME = db.0.knoe.dev,db.prole.org
|
||||
SUPABASE_API_HOSTNAME = api.0.knoe.dev
|
||||
SUPABASE_STUDIO_HOSTNAME = db.0.knoe.dev
|
||||
SUPABASE_INGRESS_CLASS = gce
|
||||
GITLAB_DOMAIN = git.knoe.dev
|
||||
GITLAB_PUBLIC_HOSTS = git.knoe.dev,git.prole.org
|
||||
GITLAB_PUBLIC_HOSTS = git.knoe.dev
|
||||
GITLAB_INGRESS_CLASS = gce
|
||||
AUTHORITY_ENABLED = true
|
||||
AUTH_HOSTNAME = api.knoe.dev
|
||||
AUTH_VERIFY_PATH = /auth/verify
|
||||
AUTH_LOGIN_PATH = /auth/login
|
||||
AUTH_RESPONSE_HEADERS = X-Prole-User,X-Prole-Email,X-Prole-Groups
|
||||
PROTECTED_GIT_HOSTS = git.knoe.dev,git.prole.org
|
||||
PROTECTED_DB_HOSTS = db.0.knoe.dev,db.prole.org
|
||||
PROTECTED_GIT_HOSTS = git.knoe.dev
|
||||
PROTECTED_DB_HOSTS = db.0.knoe.dev,api.0.knoe.dev
|
||||
|
||||
[Welcome]
|
||||
; No configuration values captured yet for this section.
|
||||
|
||||
@ -45,6 +45,65 @@ die() { echo "[ERROR] $*" >&2; exit 2; }
|
||||
log() { echo "[INFO] $*"; }
|
||||
warn() { echo "[WARN] $*" >&2; }
|
||||
|
||||
resolve_explicit_kube_context() {
|
||||
local ctx="${KUBECTL_CONTEXT:-${KUBE_CONTEXT_NAME:-${KUBECONTEXT:-}}}"
|
||||
if [[ -n "$ctx" ]]; then
|
||||
printf '%s' "$ctx"
|
||||
return 0
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
enforce_app_cluster_targeting() {
|
||||
if [[ "$MODE" != "k8s" ]]; then
|
||||
return 0
|
||||
fi
|
||||
local app_ctx="${APP_CLUSTER_KUBECONTEXT:-}"
|
||||
local db_ctx="${DB_CLUSTER_KUBECONTEXT:-}"
|
||||
local target_ctx
|
||||
target_ctx="$(resolve_explicit_kube_context || true)"
|
||||
|
||||
[[ -n "$app_ctx" ]] || die "APP_CLUSTER_KUBECONTEXT is required for k8s GitLab deployment."
|
||||
[[ -n "$target_ctx" ]] || die "Explicit kubectl context is required for k8s GitLab deployment."
|
||||
|
||||
if [[ -n "$db_ctx" && "$target_ctx" == "$db_ctx" ]]; then
|
||||
die "Refusing GitLab APP step against DB context '$target_ctx'."
|
||||
fi
|
||||
if [[ "$target_ctx" != "$app_ctx" ]]; then
|
||||
die "GitLab APP step must target APP_CLUSTER_KUBECONTEXT='${app_ctx}' (got '${target_ctx}')."
|
||||
fi
|
||||
|
||||
export KUBECTL_CONTEXT="$app_ctx"
|
||||
export KUBE_CONTEXT_NAME="$app_ctx"
|
||||
export KUBECONTEXT="$app_ctx"
|
||||
}
|
||||
|
||||
kubectl() {
|
||||
local target_ctx
|
||||
target_ctx="$(resolve_explicit_kube_context || true)"
|
||||
if [[ "$MODE" == "k8s" && -z "$target_ctx" ]]; then
|
||||
die "Explicit kubectl context is required in k8s mode."
|
||||
fi
|
||||
|
||||
local arg has_context=0
|
||||
for arg in "$@"; do
|
||||
case "$arg" in
|
||||
--context|--context=*|--server|--server=*)
|
||||
has_context=1
|
||||
break
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ -n "$target_ctx" && $has_context -eq 0 ]]; then
|
||||
command kubectl --context "$target_ctx" "$@"
|
||||
else
|
||||
command kubectl "$@"
|
||||
fi
|
||||
}
|
||||
|
||||
enforce_app_cluster_targeting
|
||||
|
||||
is_truthy() {
|
||||
case "${1:-}" in
|
||||
1|true|TRUE|True|yes|YES|on|ON|y|Y)
|
||||
@ -65,11 +124,12 @@ assert_public_ingress_targeting() {
|
||||
return 0
|
||||
fi
|
||||
|
||||
local app_ctx="${APP_CLUSTER_KUBECONTEXT:-${KUBECONTEXT:-${KUBE_CONTEXT_NAME:-${KUBECTL_CONTEXT:-}}}}"
|
||||
local app_ctx="${APP_CLUSTER_KUBECONTEXT:-}"
|
||||
local db_ctx="${DB_CLUSTER_KUBECONTEXT:-}"
|
||||
local active_ctx="${KUBECONTEXT:-${KUBE_CONTEXT_NAME:-${KUBECTL_CONTEXT:-}}}"
|
||||
if [[ -z "$active_ctx" ]]; then
|
||||
active_ctx="$(kubectl config current-context 2>/dev/null || true)"
|
||||
local active_ctx="${KUBECTL_CONTEXT:-${KUBE_CONTEXT_NAME:-${KUBECONTEXT:-}}}"
|
||||
|
||||
if [[ -z "$app_ctx" || -z "$active_ctx" ]]; then
|
||||
die "Explicit APP cluster context is required for GitLab ingress operations in k8s mode."
|
||||
fi
|
||||
|
||||
local host_count=0
|
||||
@ -98,7 +158,12 @@ assert_unique_ingress_host_claims() {
|
||||
local host_csv="${3:-}"
|
||||
[[ -n "$host_csv" ]] || return 0
|
||||
|
||||
if ! python3 - "$host_csv" "$ingress_namespace" "$ingress_name" <<'PY'
|
||||
local target_ctx="${KUBECTL_CONTEXT:-${KUBE_CONTEXT_NAME:-${KUBECONTEXT:-}}}"
|
||||
if [[ "$MODE" == "k8s" && -z "$target_ctx" ]]; then
|
||||
die "Explicit kubectl context is required for ingress ownership checks in k8s mode."
|
||||
fi
|
||||
|
||||
if ! python3 - "$host_csv" "$ingress_namespace" "$ingress_name" "$target_ctx" <<'PY'
|
||||
import json
|
||||
import subprocess
|
||||
import sys
|
||||
@ -106,9 +171,15 @@ import sys
|
||||
requested_hosts = {h.strip().lower() for h in (sys.argv[1] or "").split(",") if h.strip()}
|
||||
target_ns = sys.argv[2]
|
||||
target_name = sys.argv[3]
|
||||
target_ctx = (sys.argv[4] or "").strip()
|
||||
|
||||
cmd = ["kubectl"]
|
||||
if target_ctx:
|
||||
cmd.extend(["--context", target_ctx])
|
||||
cmd.extend(["get", "ingress", "-A", "-o", "json"])
|
||||
|
||||
try:
|
||||
raw = subprocess.check_output(["kubectl", "get", "ingress", "-A", "-o", "json"], text=True)
|
||||
raw = subprocess.check_output(cmd, text=True)
|
||||
except Exception:
|
||||
raise SystemExit(0)
|
||||
|
||||
|
||||
@ -42,6 +42,73 @@ if [[ -z "${PROLE_MODE:-}" ]]; then
|
||||
export PROLE_MODE="k3s"
|
||||
fi
|
||||
|
||||
resolve_explicit_kube_context() {
|
||||
local ctx="${KUBECTL_CONTEXT:-${KUBE_CONTEXT_NAME:-${KUBECONTEXT:-}}}"
|
||||
if [[ -n "$ctx" ]]; then
|
||||
printf '%s' "$ctx"
|
||||
return 0
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
enforce_app_cluster_targeting() {
|
||||
if [[ "${PROLE_MODE:-}" != "k8s" ]]; then
|
||||
return 0
|
||||
fi
|
||||
local app_ctx="${APP_CLUSTER_KUBECONTEXT:-}"
|
||||
local db_ctx="${DB_CLUSTER_KUBECONTEXT:-}"
|
||||
local target_ctx
|
||||
target_ctx="$(resolve_explicit_kube_context || true)"
|
||||
|
||||
if [[ -z "$app_ctx" ]]; then
|
||||
echo "ERROR: APP_CLUSTER_KUBECONTEXT is required for k8s Kong deployment." >&2
|
||||
exit 2
|
||||
fi
|
||||
if [[ -z "$target_ctx" ]]; then
|
||||
echo "ERROR: explicit kubectl context is required for k8s Kong deployment." >&2
|
||||
exit 2
|
||||
fi
|
||||
if [[ -n "$db_ctx" && "$target_ctx" == "$db_ctx" ]]; then
|
||||
echo "ERROR: refusing Kong APP step against DB context '$target_ctx'." >&2
|
||||
exit 2
|
||||
fi
|
||||
if [[ "$target_ctx" != "$app_ctx" ]]; then
|
||||
echo "ERROR: Kong APP step must target APP_CLUSTER_KUBECONTEXT='$app_ctx' (got '$target_ctx')." >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
export KUBECTL_CONTEXT="$app_ctx"
|
||||
export KUBE_CONTEXT_NAME="$app_ctx"
|
||||
export KUBECONTEXT="$app_ctx"
|
||||
}
|
||||
|
||||
kubectl() {
|
||||
local target_ctx
|
||||
target_ctx="$(resolve_explicit_kube_context || true)"
|
||||
if [[ "${PROLE_MODE:-}" == "k8s" && -z "$target_ctx" ]]; then
|
||||
echo "ERROR: explicit kubectl context is required in k8s mode." >&2
|
||||
return 2
|
||||
fi
|
||||
|
||||
local arg has_context=0
|
||||
for arg in "$@"; do
|
||||
case "$arg" in
|
||||
--context|--context=*|--server|--server=*)
|
||||
has_context=1
|
||||
break
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ -n "$target_ctx" && $has_context -eq 0 ]]; then
|
||||
command kubectl --context "$target_ctx" "$@"
|
||||
else
|
||||
command kubectl "$@"
|
||||
fi
|
||||
}
|
||||
|
||||
enforce_app_cluster_targeting
|
||||
|
||||
if [[ "${COMMON_CORE_HELP:-0}" == 1 ]]; then
|
||||
common_core_usage "$0"
|
||||
exit 0
|
||||
@ -143,11 +210,13 @@ assert_public_ingress_targeting() {
|
||||
return 0
|
||||
fi
|
||||
|
||||
local app_ctx="${APP_CLUSTER_KUBECONTEXT:-${KUBECONTEXT:-${KUBE_CONTEXT_NAME:-${KUBECTL_CONTEXT:-}}}}"
|
||||
local app_ctx="${APP_CLUSTER_KUBECONTEXT:-}"
|
||||
local db_ctx="${DB_CLUSTER_KUBECONTEXT:-}"
|
||||
local active_ctx="${KUBECONTEXT:-${KUBE_CONTEXT_NAME:-${KUBECTL_CONTEXT:-}}}"
|
||||
if [[ -z "$active_ctx" ]]; then
|
||||
active_ctx="$(kubectl config current-context 2>/dev/null || true)"
|
||||
local active_ctx="${KUBECTL_CONTEXT:-${KUBE_CONTEXT_NAME:-${KUBECONTEXT:-}}}"
|
||||
|
||||
if [[ -z "$app_ctx" || -z "$active_ctx" ]]; then
|
||||
echo "ERROR: explicit APP cluster context is required for public Kong ingress in k8s mode." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
local host_count=0
|
||||
@ -181,7 +250,13 @@ assert_unique_ingress_host_claims() {
|
||||
local host_csv="${3:-}"
|
||||
[[ -n "$host_csv" ]] || return 0
|
||||
|
||||
if ! python3 - "$host_csv" "$ingress_namespace" "$ingress_name" <<'PY'
|
||||
local target_ctx="${KUBECTL_CONTEXT:-${KUBE_CONTEXT_NAME:-${KUBECONTEXT:-}}}"
|
||||
if [[ "${PROLE_MODE:-}" == "k8s" && -z "$target_ctx" ]]; then
|
||||
echo "ERROR: explicit kubectl context is required for ingress ownership checks in k8s mode." >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
if ! python3 - "$host_csv" "$ingress_namespace" "$ingress_name" "$target_ctx" <<'PY'
|
||||
import json
|
||||
import subprocess
|
||||
import sys
|
||||
@ -189,9 +264,15 @@ import sys
|
||||
requested_hosts = {h.strip().lower() for h in (sys.argv[1] or "").split(",") if h.strip()}
|
||||
target_ns = sys.argv[2]
|
||||
target_name = sys.argv[3]
|
||||
target_ctx = (sys.argv[4] or "").strip()
|
||||
|
||||
cmd = ["kubectl"]
|
||||
if target_ctx:
|
||||
cmd.extend(["--context", target_ctx])
|
||||
cmd.extend(["get", "ingress", "-A", "-o", "json"])
|
||||
|
||||
try:
|
||||
raw = subprocess.check_output(["kubectl", "get", "ingress", "-A", "-o", "json"], text=True)
|
||||
raw = subprocess.check_output(cmd, text=True)
|
||||
except Exception:
|
||||
raise SystemExit(0)
|
||||
|
||||
|
||||
@ -392,13 +392,13 @@ class KnoeInstaller:
|
||||
else:
|
||||
cmd = ["kubectl"]
|
||||
|
||||
# 2. Add context if explicitly selected (and not already in the command)
|
||||
ctx = self._get_input("init_cluster.selected_kubectx", "").strip()
|
||||
# For k8s/prod mode, fall back to the GKE context saved in prole_cfg_data
|
||||
if not ctx and mode == "k8s":
|
||||
ctx = (
|
||||
(self.prole_cfg_data.get("Global", {}) or {}).get("KUBECONTEXT", "") or ""
|
||||
).strip()
|
||||
# 2. Add context explicitly (and never rely on ambient current-context)
|
||||
if mode == "k8s":
|
||||
# In split-cluster GKE mode default kubectl access must target APP cluster
|
||||
# unless a step uses an explicit role-specific env override.
|
||||
ctx = self._cluster_kubecontext("app").strip()
|
||||
else:
|
||||
ctx = self._get_input("init_cluster.selected_kubectx", "").strip()
|
||||
if ctx and cmd[0] == "kubectl" and "--server" not in cmd and "--context" not in cmd:
|
||||
cmd.extend(["--context", ctx])
|
||||
return cmd
|
||||
@ -1656,18 +1656,15 @@ class KnoeInstaller:
|
||||
if Path(default_kube).exists():
|
||||
env["KUBECONFIG"] = default_kube
|
||||
elif mode == "k8s":
|
||||
# For GKE/prod clusters, gcloud get-credentials writes to ~/.kube/config.
|
||||
# Read the context that was saved after cluster selection.
|
||||
gke_context = (
|
||||
self._cluster_kubecontext(cluster_role)
|
||||
or (self.prole_cfg_data.get("Global", {}) or {}).get("KUBECONTEXT", "")
|
||||
or ""
|
||||
).strip()
|
||||
# For GKE/prod split-cluster deploys we require explicit role context.
|
||||
gke_context = self._cluster_kubecontext(cluster_role).strip()
|
||||
default_kube = str(Path.home() / ".kube" / "config")
|
||||
if Path(default_kube).exists():
|
||||
env["KUBECONFIG"] = default_kube
|
||||
if gke_context:
|
||||
env["KUBECONTEXT"] = gke_context
|
||||
env["KUBE_CONTEXT_NAME"] = gke_context
|
||||
env["KUBECTL_CONTEXT"] = gke_context
|
||||
else:
|
||||
env.pop("KUBECONFIG", None)
|
||||
env["SERVICE_NAMESPACE"] = service_ns
|
||||
@ -1680,9 +1677,10 @@ class KnoeInstaller:
|
||||
env["SUPABASE_STUDIO_ENABLED"] = _bool_str(
|
||||
self._get_input_bool("init_cluster.supabase_studio_enabled", False)
|
||||
)
|
||||
default_supabase_studio_url = "db.0.knoe.dev" if mode == "k8s" else "db.prole.org"
|
||||
env["SUPABASE_STUDIO_URL"] = (
|
||||
self._get_input("init_cluster.supabase_studio_url", "db.prole.org").strip()
|
||||
or "db.prole.org"
|
||||
self._get_input("init_cluster.supabase_studio_url", default_supabase_studio_url).strip()
|
||||
or default_supabase_studio_url
|
||||
)
|
||||
env["SUPABASE_AUTH_ENABLED"] = _bool_str(
|
||||
self._get_input_bool("init_cluster.supabase_auth_enabled", True)
|
||||
@ -1861,6 +1859,7 @@ class KnoeInstaller:
|
||||
return str(mode).strip() or DEFAULT_DB_CLUSTER_MODE
|
||||
|
||||
def _cluster_kubecontext(self, role: str) -> str:
|
||||
role = "app" if str(role).strip().lower() == "app" else "db"
|
||||
key = (
|
||||
"init_cluster.app_cluster_kubecontext"
|
||||
if role == "app"
|
||||
@ -1872,12 +1871,40 @@ class KnoeInstaller:
|
||||
else "env_setup.DB_CLUSTER_KUBECONTEXT"
|
||||
)
|
||||
context = _safe_str(self._get_input(key, "") or self._get_input(fallback_key, ""))
|
||||
if context:
|
||||
return context
|
||||
|
||||
glob = self.prole_cfg_data.get("Global", {}) or {}
|
||||
global_key = "APP_CLUSTER_KUBECONTEXT" if role == "app" else "DB_CLUSTER_KUBECONTEXT"
|
||||
context = _safe_str(glob.get(global_key, ""))
|
||||
if not context:
|
||||
context = _safe_str(glob.get(global_key, ""))
|
||||
|
||||
opposite_key = (
|
||||
"init_cluster.db_cluster_kubecontext"
|
||||
if role == "app"
|
||||
else "init_cluster.app_cluster_kubecontext"
|
||||
)
|
||||
opposite_fallback_key = (
|
||||
"env_setup.DB_CLUSTER_KUBECONTEXT"
|
||||
if role == "app"
|
||||
else "env_setup.APP_CLUSTER_KUBECONTEXT"
|
||||
)
|
||||
opposite_global_key = "DB_CLUSTER_KUBECONTEXT" if role == "app" else "APP_CLUSTER_KUBECONTEXT"
|
||||
opposite_context = _safe_str(
|
||||
self._get_input(opposite_key, "")
|
||||
or self._get_input(opposite_fallback_key, "")
|
||||
or glob.get(opposite_global_key, "")
|
||||
)
|
||||
if context and opposite_context and context == opposite_context:
|
||||
raise RuntimeError(
|
||||
f"Refusing {role.upper()} targeting: APP/DB contexts must differ, both resolve to '{context}'."
|
||||
)
|
||||
|
||||
if self._deployment_mode() == "k8s":
|
||||
if not context:
|
||||
required_key = "APP_CLUSTER_KUBECONTEXT" if role == "app" else "DB_CLUSTER_KUBECONTEXT"
|
||||
raise RuntimeError(
|
||||
f"Missing required {required_key} for {role.upper()} cluster operation in k8s mode."
|
||||
)
|
||||
return context
|
||||
|
||||
if context:
|
||||
return context
|
||||
|
||||
@ -3984,6 +4011,21 @@ class KnoeConsoleInstaller(KnoeInstaller):
|
||||
def _run_cmd(
|
||||
self, cmd, cwd=None, env=None, stdin_text=None, on_stdout=None, on_stderr=None
|
||||
) -> int:
|
||||
if isinstance(cmd, (list, tuple)) and cmd:
|
||||
binary = Path(str(cmd[0])).name
|
||||
if binary in {"kubectl", "helm"}:
|
||||
target_role = "UNKNOWN"
|
||||
target_ctx = ""
|
||||
if isinstance(env, dict):
|
||||
target_role = str(env.get("KNOE_CLUSTER_ROLE") or "").strip().upper() or "UNKNOWN"
|
||||
target_ctx = str(
|
||||
env.get("KUBECTL_CONTEXT")
|
||||
or env.get("KUBE_CONTEXT_NAME")
|
||||
or env.get("KUBECONTEXT")
|
||||
or ""
|
||||
).strip()
|
||||
self.log(f"[TARGET {target_role}] {binary} context={target_ctx or '<unset>'}")
|
||||
|
||||
stdin_handle = subprocess.PIPE if stdin_text else None
|
||||
if isinstance(cmd, str):
|
||||
proc = subprocess.Popen(
|
||||
@ -4054,6 +4096,21 @@ class KnoeConsoleInstaller(KnoeInstaller):
|
||||
if mode:
|
||||
mode_args = ["--mode", mode]
|
||||
|
||||
target_role = "UNKNOWN"
|
||||
target_ctx = ""
|
||||
if isinstance(env, dict):
|
||||
target_role = str(env.get("KNOE_CLUSTER_ROLE") or "").strip().upper() or "UNKNOWN"
|
||||
target_ctx = str(
|
||||
env.get("KUBECTL_CONTEXT")
|
||||
or env.get("KUBE_CONTEXT_NAME")
|
||||
or env.get("KUBECONTEXT")
|
||||
or ""
|
||||
).strip()
|
||||
if target_ctx or target_role != "UNKNOWN":
|
||||
self.log(
|
||||
f"[TARGET {target_role}] script={script_name} context={target_ctx or '<unset>'}"
|
||||
)
|
||||
|
||||
return self.controller.run_script(
|
||||
script_name,
|
||||
args=mode_args + (args or []),
|
||||
@ -7055,9 +7112,12 @@ class KnoeConsoleInstaller(KnoeInstaller):
|
||||
Uses ``supabase_studio_url`` as the source of truth for ingress host.
|
||||
Operates only on Studio resources — does not restart other Supabase components.
|
||||
"""
|
||||
default_supabase_studio_url = (
|
||||
"db.0.knoe.dev" if self._deployment_mode() == "k8s" else "db.prole.org"
|
||||
)
|
||||
studio_url = (
|
||||
self._get_input("init_cluster.supabase_studio_url", "db.prole.org").strip()
|
||||
or "db.prole.org"
|
||||
self._get_input("init_cluster.supabase_studio_url", default_supabase_studio_url).strip()
|
||||
or default_supabase_studio_url
|
||||
)
|
||||
supabase_ns = (os.environ.get("SUPABASE_NAMESPACE") or "").strip() or "supabase"
|
||||
self.log(f"[REPAIR] Reconciling Supabase Studio → ingress host: {studio_url}")
|
||||
|
||||
@ -2097,8 +2097,9 @@ def _render_prole_cfg(
|
||||
get_section("Global", "supabase_hostname"),
|
||||
get_section("Global", "SUPABASE_HOSTNAME"),
|
||||
)
|
||||
default_supabase_hostname = "db.0.knoe.dev" if runtime_mode == "prod" else "db.prole.org"
|
||||
if not base_supabase_hostname:
|
||||
base_supabase_hostname = "db.prole.org"
|
||||
base_supabase_hostname = default_supabase_hostname
|
||||
|
||||
def derive_value(current: str, derived: str, placeholder: str) -> str:
|
||||
"""Return a placeholder when `current` is unset or matches the derived value."""
|
||||
@ -2162,7 +2163,7 @@ def _render_prole_cfg(
|
||||
user_section["SERVICE_HOSTNAME"] = base_service_hostname
|
||||
|
||||
# Canonical public hostname for Supabase (front-door)
|
||||
if base_supabase_hostname and base_supabase_hostname != "db.prole.org":
|
||||
if base_supabase_hostname and base_supabase_hostname != default_supabase_hostname:
|
||||
user_section["supabase_hostname"] = base_supabase_hostname
|
||||
|
||||
# Derived values for repeated touch-points
|
||||
|
||||
@ -53,15 +53,44 @@ def build_kubectl_env_for_cluster(
|
||||
cluster_role: str,
|
||||
) -> dict:
|
||||
env = dict(base_env or os.environ)
|
||||
if kubecontext:
|
||||
env["KUBECONTEXT"] = kubecontext
|
||||
env["KUBE_CONTEXT_NAME"] = kubecontext
|
||||
env["KUBECTL_CONTEXT"] = kubecontext
|
||||
role = (cluster_role or "").strip().lower()
|
||||
if role not in {"app", "db"}:
|
||||
raise ValueError(f"Unsupported cluster role '{cluster_role}'. Expected 'app' or 'db'.")
|
||||
|
||||
required_key = "APP_CLUSTER_KUBECONTEXT" if role == "app" else "DB_CLUSTER_KUBECONTEXT"
|
||||
opposite_key = "DB_CLUSTER_KUBECONTEXT" if role == "app" else "APP_CLUSTER_KUBECONTEXT"
|
||||
required_ctx = str(env.get(required_key) or "").strip()
|
||||
opposite_ctx = str(env.get(opposite_key) or "").strip()
|
||||
explicit_ctx = str(kubecontext or "").strip() or required_ctx
|
||||
|
||||
if not explicit_ctx:
|
||||
raise ValueError(
|
||||
f"Missing required kube context for {role.upper()} cluster targeting. "
|
||||
f"Set {required_key} and pass an explicit context."
|
||||
)
|
||||
if opposite_ctx and explicit_ctx == opposite_ctx:
|
||||
raise ValueError(
|
||||
f"Refusing {role.upper()} operation: kube context '{explicit_ctx}' matches "
|
||||
f"{opposite_key}."
|
||||
)
|
||||
if required_ctx and explicit_ctx != required_ctx:
|
||||
raise ValueError(
|
||||
f"Refusing {role.upper()} operation with kube context '{explicit_ctx}': "
|
||||
f"expected {required_key}='{required_ctx}'."
|
||||
)
|
||||
|
||||
env["KUBECTEXT"] = explicit_ctx
|
||||
env["KUBE_CONTEXT_NAME"] = explicit_ctx
|
||||
env["KUBECTL_CONTEXT"] = explicit_ctx
|
||||
if role == "app":
|
||||
env.setdefault("APP_CLUSTER_KUBECONTEXT", explicit_ctx)
|
||||
else:
|
||||
env.setdefault("DB_CLUSTER_KUBECONTEXT", explicit_ctx)
|
||||
env["CLUSTER_NAME"] = cluster_name
|
||||
env["KNOE_CLUSTER_ROLE"] = cluster_role
|
||||
if cluster_role == "app":
|
||||
env["KNOE_CLUSTER_ROLE"] = role
|
||||
if role == "app":
|
||||
env["KNOE_APP_CLUSTER_NAME"] = cluster_name
|
||||
elif cluster_role == "db":
|
||||
elif role == "db":
|
||||
env["KNOE_DB_CLUSTER_NAME"] = cluster_name
|
||||
return env
|
||||
|
||||
|
||||
@ -141,6 +141,18 @@ def _normalize_hostname(raw_value: str, fallback: str) -> str:
|
||||
return token.split("/", 1)[0].strip() or fallback
|
||||
|
||||
|
||||
def _derive_api_hostname_from_studio(studio_hostname_raw: str, auth_hostname: str, mode: str) -> str:
|
||||
if mode != "k8s":
|
||||
return studio_hostname_raw
|
||||
|
||||
normalized_studio = _normalize_hostname(studio_hostname_raw, fallback="db.0.knoe.dev")
|
||||
if mode == "k8s":
|
||||
if normalized_studio.startswith("db."):
|
||||
return f"api.{normalized_studio[3:]}"
|
||||
return auth_hostname
|
||||
return studio_hostname_raw
|
||||
|
||||
|
||||
def _validate_unique_ingress_claims(claims: dict[str, list[str]]) -> None:
|
||||
claimed: dict[tuple[str, str], str] = {}
|
||||
for owner, hosts in claims.items():
|
||||
@ -323,7 +335,7 @@ def _build_overlay(cfg: configparser.ConfigParser, args: argparse.Namespace) ->
|
||||
if not studio_hostname_raw:
|
||||
studio_hostname_raw = legacy_supabase_hostname_raw
|
||||
|
||||
api_host_fallback = auth_hostname if mode == "k8s" else studio_hostname_raw
|
||||
api_host_fallback = _derive_api_hostname_from_studio(studio_hostname_raw, auth_hostname, mode)
|
||||
|
||||
api_hostname_raw = _first(
|
||||
os.environ.get("SUPABASE_API_URL", ""),
|
||||
|
||||
297
tests/etc/test_init_monitoring_k8s_storage_class_validation.sh
Normal file
297
tests/etc/test_init_monitoring_k8s_storage_class_validation.sh
Normal file
@ -0,0 +1,297 @@
|
||||
#!/usr/bin/env bash
|
||||
# Unit test for etc/init_monitoring.sh (k8s mode): storage class selection must be
|
||||
# cluster-aware and fail early when configured classes do not exist.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
|
||||
PROLE_HOME=$(cd "$SCRIPT_DIR/../.." && pwd)
|
||||
ETC_DIR="$PROLE_HOME/etc"
|
||||
SCRIPT_UNDER_TEST="$ETC_DIR/init_monitoring.sh"
|
||||
|
||||
TMP_DIR=$(mktemp -d)
|
||||
trap 'rm -rf "$TMP_DIR"' EXIT
|
||||
|
||||
MOCK_CALLS_LOG="$TMP_DIR/mock_calls.log"
|
||||
HELM_STATUS_DIR="$TMP_DIR/helm_status"
|
||||
NS_EXISTS_FILE="$TMP_DIR/ns_exists"
|
||||
|
||||
mkdir -p "$HELM_STATUS_DIR"
|
||||
echo "pending-install" > "$HELM_STATUS_DIR/kps"
|
||||
echo "1" > "$NS_EXISTS_FILE"
|
||||
|
||||
export MOCK_CALLS_LOG
|
||||
export HELM_STATUS_DIR
|
||||
export NS_EXISTS_FILE
|
||||
export TMP_DIR
|
||||
|
||||
write_mock() {
|
||||
local name="$1"
|
||||
cat > "$TMP_DIR/$name"
|
||||
chmod +x "$TMP_DIR/$name"
|
||||
}
|
||||
|
||||
write_mock kubectl <<'EOF'
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
echo "Mocked kubectl called with $*" >> "$MOCK_CALLS_LOG"
|
||||
|
||||
if [[ "${1:-}" == "-n" ]]; then
|
||||
shift 2
|
||||
fi
|
||||
|
||||
if [[ "${1:-}" == "get" && "${2:-}" == "namespace" && "${3:-}" == "monitoring" ]]; then
|
||||
if [[ "$(cat "$NS_EXISTS_FILE" 2>/dev/null || echo 0)" == "1" ]]; then
|
||||
exit 0
|
||||
fi
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "${1:-}" == "create" && "${2:-}" == "namespace" && "${3:-}" == "monitoring" ]]; then
|
||||
echo 1 > "$NS_EXISTS_FILE"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [[ "${1:-}" == "delete" && "${2:-}" == "namespace" && "${3:-}" == "monitoring" ]]; then
|
||||
echo 0 > "$NS_EXISTS_FILE"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [[ "${1:-}" == "get" && "${2:-}" == "pvc" && "${3:-}" == "-o" && "${4:-}" == "json" ]]; then
|
||||
echo '{"items":[]}'
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [[ "${1:-}" == "get" && "${2:-}" == "nodes" ]]; then
|
||||
echo "node/prole-gke-node-0"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [[ "${1:-}" == "get" && "${2:-}" == "storageclass" && "${3:-}" == "-o" ]]; then
|
||||
printf '%s\n' "${DEFAULT_STORAGE_CLASS:-}"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [[ "${1:-}" == "get" && "${2:-}" == "storageclass" ]]; then
|
||||
target="${3:-}"
|
||||
IFS=',' read -r -a scs <<< "${AVAILABLE_STORAGE_CLASSES:-}"
|
||||
for sc in "${scs[@]}"; do
|
||||
if [[ "$sc" == "$target" ]]; then
|
||||
exit 0
|
||||
fi
|
||||
done
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "${1:-}" == "apply" ]]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
exit 0
|
||||
EOF
|
||||
|
||||
write_mock helm <<'EOF'
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
echo "Mocked helm called with $*" >> "$MOCK_CALLS_LOG"
|
||||
|
||||
cmd="${1:-}"
|
||||
shift || true
|
||||
|
||||
status_file_for_release() {
|
||||
local release="$1"
|
||||
echo "$HELM_STATUS_DIR/$release"
|
||||
}
|
||||
|
||||
case "$cmd" in
|
||||
status)
|
||||
release="${1:-}"
|
||||
st="$(cat "$(status_file_for_release "$release")" 2>/dev/null || true)"
|
||||
if [[ -z "$st" ]]; then
|
||||
echo '{}'
|
||||
exit 0
|
||||
fi
|
||||
echo '{"info":{"status":"'"$st"'"}}'
|
||||
;;
|
||||
history)
|
||||
echo "REVISION UPDATED STATUS CHART"
|
||||
echo "1 2026-01-01 00:00:00 unknown kube-prometheus-stack"
|
||||
;;
|
||||
repo)
|
||||
exit 0
|
||||
;;
|
||||
upgrade)
|
||||
release=""
|
||||
if [[ "${1:-}" == "--install" ]]; then
|
||||
release="${2:-}"
|
||||
else
|
||||
release="${1:-}"
|
||||
fi
|
||||
|
||||
for ((i=1; i<=$#; i++)); do
|
||||
if [[ "${!i}" == "-f" ]]; then
|
||||
j=$((i+1))
|
||||
vf="${!j:-}"
|
||||
if [[ -n "${vf}" && -f "${vf}" && -n "${release}" ]]; then
|
||||
cp "${vf}" "${TMP_DIR}/helm_values_${release}.yaml" 2>/dev/null || true
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ -n "$release" ]]; then
|
||||
echo "deployed" > "$(status_file_for_release "$release")"
|
||||
fi
|
||||
echo "Release upgraded"
|
||||
exit 0
|
||||
;;
|
||||
uninstall)
|
||||
release="${1:-}"
|
||||
if [[ -n "$release" ]]; then
|
||||
: > "$(status_file_for_release "$release")"
|
||||
fi
|
||||
echo "Release uninstalled"
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
exit 0
|
||||
;;
|
||||
esac
|
||||
EOF
|
||||
|
||||
write_mock jq <<'EOF'
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
echo "Mocked jq called with $*" >> "$MOCK_CALLS_LOG"
|
||||
|
||||
query=""
|
||||
for arg in "$@"; do
|
||||
query="$arg"
|
||||
done
|
||||
|
||||
input="$(cat)"
|
||||
|
||||
if [[ "$query" == *".info.status"* ]]; then
|
||||
echo "$input" | sed -n 's/.*"status"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p'
|
||||
exit 0
|
||||
fi
|
||||
|
||||
exit 0
|
||||
EOF
|
||||
|
||||
for tool in curl docker k3d ansible-playbook tofu terraform ollama; do
|
||||
cat > "$TMP_DIR/$tool" <<EOF
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
echo "Mocked $tool called with \$*" >> "$MOCK_CALLS_LOG"
|
||||
exit 0
|
||||
EOF
|
||||
chmod +x "$TMP_DIR/$tool"
|
||||
done
|
||||
|
||||
mkdir -p "$TMP_DIR/conf"
|
||||
cat <<C_EOF > "$TMP_DIR/conf/prole.cfg"
|
||||
[globals]
|
||||
prole.home = $PROLE_HOME
|
||||
prole.mode = k8s
|
||||
[k3s]
|
||||
server = https://localhost:6443
|
||||
token = test-token
|
||||
C_EOF
|
||||
|
||||
export PATH="$TMP_DIR:$PATH"
|
||||
export PROLE_HOME="$PROLE_HOME"
|
||||
export PROLE_CONF="$TMP_DIR/conf"
|
||||
export PROLE_SERVICE="$PROLE_HOME"
|
||||
export NAMESPACE="test-ns"
|
||||
export PROLE_PASSWD="test-password"
|
||||
|
||||
export HELM_WAIT_TIMEOUT=1
|
||||
export HELM_WAIT_INTERVAL=0.2
|
||||
export MONITORING_RESET_TIMEOUT=2
|
||||
export MONITORING_RESET_INTERVAL=0.2
|
||||
export MONITORING_STABILIZE_TIMEOUT=1
|
||||
export MONITORING_READY_TIMEOUT=1s
|
||||
export MONITORING_RESET_RETRIES=1
|
||||
export HELM_UPGRADE_RETRIES=2
|
||||
export HELM_RETRY_DELAY=0
|
||||
export MONITORING_HELM_TIMEOUT=1s
|
||||
|
||||
run_initialize() {
|
||||
local label="$1"
|
||||
set +e
|
||||
bash "$SCRIPT_UNDER_TEST" initialize > "$TMP_DIR/${label}.stdout" 2> "$TMP_DIR/${label}.stderr"
|
||||
local rc=$?
|
||||
set -e
|
||||
echo "$rc"
|
||||
}
|
||||
|
||||
# Scenario 1: k8s mode should use an existing default StorageClass and avoid k3s-local fallbacks.
|
||||
export DEFAULT_STORAGE_CLASS="garage-hdd"
|
||||
export AVAILABLE_STORAGE_CLASSES="garage-hdd"
|
||||
unset MONITORING_STORAGE_CLASS || true
|
||||
|
||||
RC=$(run_initialize "default_sc")
|
||||
if [[ "$RC" -ne 0 ]]; then
|
||||
echo "FAILURE: expected success for default storage class scenario"
|
||||
echo "--- stderr ---"
|
||||
cat "$TMP_DIR/default_sc.stderr"
|
||||
echo "--- stdout ---"
|
||||
cat "$TMP_DIR/default_sc.stdout"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ ! -f "$TMP_DIR/helm_values_kps.yaml" ]]; then
|
||||
echo "FAILURE: expected helm values file for kps release"
|
||||
ls -la "$TMP_DIR" || true
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! grep -q "storageClassName: garage-hdd" "$TMP_DIR/helm_values_kps.yaml"; then
|
||||
echo "FAILURE: expected rendered monitoring storageClassName to use default class garage-hdd"
|
||||
sed -n '1,260p' "$TMP_DIR/helm_values_kps.yaml" || true
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if grep -q "merlin-local-iscsi" "$TMP_DIR/helm_values_kps.yaml"; then
|
||||
echo "FAILURE: did not expect k3s-local storage class names in k8s mode values"
|
||||
sed -n '1,260p' "$TMP_DIR/helm_values_kps.yaml" || true
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Scenario 2: configured stale class must fail early with a clear error.
|
||||
echo "pending-install" > "$HELM_STATUS_DIR/kps"
|
||||
echo "" > "$HELM_STATUS_DIR/grafana"
|
||||
rm -f "$TMP_DIR/helm_values_kps.yaml" "$TMP_DIR/helm_values_grafana.yaml"
|
||||
|
||||
export MONITORING_STORAGE_CLASS="merlin-local-iscsi-d004-grafana"
|
||||
export AVAILABLE_STORAGE_CLASSES="garage-hdd"
|
||||
|
||||
RC=$(run_initialize "missing_sc")
|
||||
if [[ "$RC" -eq 0 ]]; then
|
||||
echo "FAILURE: expected failure for missing configured monitoring storage class"
|
||||
echo "--- stderr ---"
|
||||
cat "$TMP_DIR/missing_sc.stderr"
|
||||
echo "--- stdout ---"
|
||||
cat "$TMP_DIR/missing_sc.stdout"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! grep -q "Monitoring storageClass validation failed" "$TMP_DIR/missing_sc.stderr"; then
|
||||
echo "FAILURE: expected clear validation error message for missing storage class"
|
||||
echo "--- stderr ---"
|
||||
cat "$TMP_DIR/missing_sc.stderr"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! grep -q "merlin-local-iscsi-d004-grafana" "$TMP_DIR/missing_sc.stderr"; then
|
||||
echo "FAILURE: expected missing stale storage class name in error output"
|
||||
echo "--- stderr ---"
|
||||
cat "$TMP_DIR/missing_sc.stderr"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "SUCCESS"
|
||||
exit 0
|
||||
65
tests/etc/test_prole_cfg_hyphenated_keys_normalized.sh
Normal file
65
tests/etc/test_prole_cfg_hyphenated_keys_normalized.sh
Normal file
@ -0,0 +1,65 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# Regression test: config keys containing hyphens must be normalized to
|
||||
# shell-safe variable names instead of crashing with "invalid variable name".
|
||||
|
||||
SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
|
||||
PROLE_HOME=$(cd "$SCRIPT_DIR/../.." && pwd)
|
||||
|
||||
TMP_DIR=$(mktemp -d)
|
||||
trap 'rm -rf "$TMP_DIR"' EXIT
|
||||
|
||||
BIN_DIR="$TMP_DIR/bin"
|
||||
mkdir -p "$BIN_DIR"
|
||||
|
||||
cat >"$BIN_DIR/kubectl" <<'EOF'
|
||||
#!/usr/bin/env bash
|
||||
exit 0
|
||||
EOF
|
||||
chmod +x "$BIN_DIR/kubectl"
|
||||
|
||||
cat >"$BIN_DIR/kubectx" <<'EOF'
|
||||
#!/usr/bin/env bash
|
||||
exit 0
|
||||
EOF
|
||||
chmod +x "$BIN_DIR/kubectx"
|
||||
|
||||
cat >"$BIN_DIR/kubens" <<'EOF'
|
||||
#!/usr/bin/env bash
|
||||
exit 0
|
||||
EOF
|
||||
chmod +x "$BIN_DIR/kubens"
|
||||
|
||||
export PATH="$BIN_DIR:$PATH"
|
||||
|
||||
CONF_DIR="$TMP_DIR/conf"
|
||||
mkdir -p "$CONF_DIR/service"
|
||||
|
||||
cat >"$CONF_DIR/service/prole.cfg" <<EOF
|
||||
[Global]
|
||||
DEPLOYMENT_MODE = local
|
||||
NAMESPACE = test-ns
|
||||
PROLE_HOME = $PROLE_HOME
|
||||
PROLE_SERVICE = $TMP_DIR/service
|
||||
dependencies_gke-gcloud-auth-plugin_install = 1
|
||||
EOF
|
||||
|
||||
ln -snf "service/prole.cfg" "$CONF_DIR/prole.cfg"
|
||||
|
||||
export PROLE_CONF="$CONF_DIR"
|
||||
export PROLE_HOME
|
||||
export PROLE_SERVICE="$TMP_DIR/service"
|
||||
|
||||
# shellcheck disable=SC1090
|
||||
source "$PROLE_HOME/etc/prole_cfg.sh"
|
||||
|
||||
expected="1"
|
||||
actual="${dependencies_gke_gcloud_auth_plugin_install:-}"
|
||||
if [[ "$actual" != "$expected" ]]; then
|
||||
echo "FAILURE: expected normalized key dependencies_gke_gcloud_auth_plugin_install=$expected, got '${actual:-<unset>}'" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "SUCCESS"
|
||||
@ -1,5 +1,6 @@
|
||||
from pathlib import Path
|
||||
import sys
|
||||
import pytest
|
||||
|
||||
PROJECT_ROOT = Path(__file__).resolve().parents[2]
|
||||
if str(PROJECT_ROOT) not in sys.path:
|
||||
@ -25,6 +26,39 @@ def test_build_kubectl_env_for_cluster_sets_explicit_role_and_context():
|
||||
assert env["KNOE_APP_CLUSTER_NAME"] == "knoe-dev-0"
|
||||
|
||||
|
||||
def test_build_kubectl_env_for_cluster_requires_explicit_context_when_missing():
|
||||
with pytest.raises(ValueError, match="Missing required kube context"):
|
||||
gke_clusters.build_kubectl_env_for_cluster(
|
||||
base_env={},
|
||||
kubecontext="",
|
||||
cluster_name="knoe-dev-0",
|
||||
cluster_role="app",
|
||||
)
|
||||
|
||||
|
||||
def test_build_kubectl_env_for_cluster_rejects_mismatched_required_role_context():
|
||||
with pytest.raises(ValueError, match="expected APP_CLUSTER_KUBECONTEXT"):
|
||||
gke_clusters.build_kubectl_env_for_cluster(
|
||||
base_env={"APP_CLUSTER_KUBECONTEXT": "ctx-app"},
|
||||
kubecontext="ctx-other",
|
||||
cluster_name="knoe-dev-0",
|
||||
cluster_role="app",
|
||||
)
|
||||
|
||||
|
||||
def test_build_kubectl_env_for_cluster_rejects_cross_role_context_usage():
|
||||
with pytest.raises(ValueError, match="matches DB_CLUSTER_KUBECONTEXT"):
|
||||
gke_clusters.build_kubectl_env_for_cluster(
|
||||
base_env={
|
||||
"APP_CLUSTER_KUBECONTEXT": "ctx-app",
|
||||
"DB_CLUSTER_KUBECONTEXT": "ctx-db",
|
||||
},
|
||||
kubecontext="ctx-db",
|
||||
cluster_name="knoe-dev-0",
|
||||
cluster_role="app",
|
||||
)
|
||||
|
||||
|
||||
def test_get_cluster_credentials_builds_expected_gcloud_command(monkeypatch):
|
||||
called = {}
|
||||
|
||||
|
||||
@ -108,7 +108,7 @@ def test_render_supabase_splits_api_and_studio_hosts_for_k8s(tmp_path: Path, mon
|
||||
[Global]
|
||||
NAMESPACE = test-ns
|
||||
DEPLOYMENT_MODE = k8s
|
||||
SUPABASE_API_HOSTNAME = api.knoe.dev
|
||||
SUPABASE_API_HOSTNAME = api.0.knoe.dev
|
||||
SUPABASE_STUDIO_HOSTNAME = db.0.knoe.dev
|
||||
STORAGE_BACKEND = local
|
||||
""",
|
||||
@ -118,15 +118,52 @@ def test_render_supabase_splits_api_and_studio_hosts_for_k8s(tmp_path: Path, mon
|
||||
args = Namespace(output_dir=str(tmp_path / "gen"), manifests_dir=str(tmp_path / "k8s"))
|
||||
overlay, _meta = _build_overlay(cfg, args)
|
||||
|
||||
assert overlay["ingress"]["hosts"][0]["host"] == "api.knoe.dev"
|
||||
assert overlay["ingress"]["hosts"][0]["host"] == "api.0.knoe.dev"
|
||||
assert overlay["studioIngress"]["hosts"][0]["host"] == "db.0.knoe.dev"
|
||||
assert overlay["ingress"]["className"] == "gce"
|
||||
assert overlay["studioIngress"]["className"] == "gce"
|
||||
assert overlay["environment"]["auth"]["API_EXTERNAL_URL"] == "https://api.knoe.dev"
|
||||
assert overlay["environment"]["auth"]["API_EXTERNAL_URL"] == "https://api.0.knoe.dev"
|
||||
assert overlay["environment"]["auth"]["GOTRUE_SITE_URL"] == "https://db.0.knoe.dev"
|
||||
assert overlay["environment"]["studio"]["SUPABASE_PUBLIC_URL"] == "https://db.0.knoe.dev"
|
||||
assert overlay["environment"]["auth"]["GOTRUE_URI_ALLOW_LIST"] == (
|
||||
"https://db.0.knoe.dev/**,https://api.knoe.dev/**"
|
||||
"https://db.0.knoe.dev/**,https://api.0.knoe.dev/**"
|
||||
)
|
||||
|
||||
|
||||
def test_render_supabase_derives_env_indexed_api_host_for_k8s(tmp_path: Path, monkeypatch):
|
||||
monkeypatch.delenv("SUPABASE_HOST", raising=False)
|
||||
monkeypatch.delenv("SUPABASE_HOSTNAME", raising=False)
|
||||
monkeypatch.delenv("SUPABASE_API_HOSTNAME", raising=False)
|
||||
monkeypatch.delenv("KNOE_DB_NAMESPACE", raising=False)
|
||||
monkeypatch.delenv("KNOE_DB_SERVICE", raising=False)
|
||||
|
||||
cfg_path = tmp_path / "prole.cfg"
|
||||
_write_cfg(
|
||||
cfg_path,
|
||||
"""
|
||||
[Inputs]
|
||||
init_password.db_password = test-password
|
||||
init_password.db_host_port = 5432
|
||||
|
||||
[Global]
|
||||
NAMESPACE = test-ns
|
||||
DEPLOYMENT_MODE = k8s
|
||||
AUTH_HOSTNAME = api.knoe.dev
|
||||
SUPABASE_STUDIO_HOSTNAME = db.33.knoe.dev
|
||||
STORAGE_BACKEND = local
|
||||
""",
|
||||
)
|
||||
|
||||
cfg = _read_cfg(cfg_path)
|
||||
args = Namespace(output_dir=str(tmp_path / "gen"), manifests_dir=str(tmp_path / "k8s"))
|
||||
overlay, _meta = _build_overlay(cfg, args)
|
||||
|
||||
assert overlay["ingress"]["hosts"][0]["host"] == "api.33.knoe.dev"
|
||||
assert overlay["studioIngress"]["hosts"][0]["host"] == "db.33.knoe.dev"
|
||||
assert overlay["environment"]["auth"]["API_EXTERNAL_URL"] == "https://api.33.knoe.dev"
|
||||
assert overlay["environment"]["auth"]["GOTRUE_SITE_URL"] == "https://db.33.knoe.dev"
|
||||
assert overlay["environment"]["auth"]["GOTRUE_URI_ALLOW_LIST"] == (
|
||||
"https://db.33.knoe.dev/**,https://api.33.knoe.dev/**"
|
||||
)
|
||||
|
||||
|
||||
@ -184,7 +221,7 @@ def test_render_supabase_rejects_db_cluster_context_for_public_ingress(tmp_path:
|
||||
[Global]
|
||||
NAMESPACE = test-ns
|
||||
DEPLOYMENT_MODE = k8s
|
||||
SUPABASE_API_HOSTNAME = api.knoe.dev
|
||||
SUPABASE_API_HOSTNAME = api.0.knoe.dev
|
||||
SUPABASE_STUDIO_HOSTNAME = db.0.knoe.dev
|
||||
STORAGE_BACKEND = local
|
||||
""",
|
||||
@ -218,7 +255,7 @@ def test_render_supabase_rejects_traefik_class_in_k8s_without_opt_in(tmp_path: P
|
||||
[Global]
|
||||
NAMESPACE = test-ns
|
||||
DEPLOYMENT_MODE = k8s
|
||||
SUPABASE_API_HOSTNAME = api.knoe.dev
|
||||
SUPABASE_API_HOSTNAME = api.0.knoe.dev
|
||||
SUPABASE_STUDIO_HOSTNAME = db.0.knoe.dev
|
||||
SUPABASE_INGRESS_CLASS = traefik
|
||||
STORAGE_BACKEND = local
|
||||
|
||||
@ -61,6 +61,7 @@ init_password.db_host_port = 5432
|
||||
{extra}
|
||||
[Global]
|
||||
NAMESPACE = test-ns
|
||||
STORAGE_BACKEND = local
|
||||
""",
|
||||
)
|
||||
return _read_cfg(cfg_path)
|
||||
@ -356,7 +357,7 @@ def test_default_inputs_contains_supabase_component_flags(tmp_path):
|
||||
for key in expected_keys:
|
||||
assert key in defaults, f"Missing default for {key!r}"
|
||||
|
||||
assert defaults["init_cluster.supabase_studio_url"] == "db.prole.org"
|
||||
assert defaults["init_cluster.supabase_studio_url"] == "db.0.knoe.dev"
|
||||
assert defaults["init_cluster.supabase_studio_enabled"] in ("false", "False", False)
|
||||
|
||||
|
||||
@ -548,8 +549,8 @@ def test_supabase_deploy_includes_knoe_schema_and_lint_remediation_sql():
|
||||
assert "GRANT CREATE ON DATABASE postgres TO supabase_storage_admin;" in script
|
||||
assert "GRANT USAGE, CREATE ON SCHEMA public TO supabase_storage_admin;" in script
|
||||
assert "ALTER EXTENSION %I SET SCHEMA knoe" in script
|
||||
assert "ALTER TABLE public.spatial_ref_sys ENABLE ROW LEVEL SECURITY;" in script
|
||||
assert "CREATE POLICY spatial_ref_sys_select_all ON public.spatial_ref_sys" in script
|
||||
assert "ALTER TABLE %I.spatial_ref_sys ENABLE ROW LEVEL SECURITY" in script
|
||||
assert "CREATE POLICY spatial_ref_sys_select_all ON %I.spatial_ref_sys" in script
|
||||
|
||||
|
||||
def test_reset_k3s_namespace_delete_db_deletes_pvcs_and_bound_pvs(tmp_path, monkeypatch):
|
||||
|
||||
Loading…
Reference in New Issue
Block a user