Make init-script secret checks warning-only and unblock Next

Co-authored-by: Junie <junie@jetbrains.com>
This commit is contained in:
chrisfu 2026-03-30 01:22:14 -07:00
parent 5a0f8ab4f8
commit c65d336d1f
4 changed files with 348 additions and 22 deletions

View File

@ -1090,13 +1090,17 @@ run_test() {
run_kerberos_test_loop() {
local test_attempt=1
local test_max_attempts=2
local checker_mode_args=()
if [[ -n "$mode" ]]; then
checker_mode_args=(--mode "$mode")
fi
while true; do
if PROLE_USE_CHILD_REALM="$test_use_child" \
KRB5_REALM="$KRB5_REALM" KRB5_KDC="$effective_kdc" KRB5_ADMIN="$KRB5_ADMIN" \
KRB5_USER="${KERBEROS_TEST_ADMIN_USER:-administrator}" KRB5_PASSWORD="$KRB5_PASSWORD" \
SAMBA_ADMIN_USER="${KERBEROS_TEST_ADMIN_USER:-administrator}" SAMBA_ADMIN_PASSWORD="$KRB5_PASSWORD" \
SAMBA_DNS_SERVER="$test_samba_dns" \
"$kerberos_check_script" test; then
"$kerberos_check_script" "${checker_mode_args[@]}" test; then
return 0
fi

View File

@ -10,6 +10,10 @@ set -euo pipefail
SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
ETC_DIR=$(cd "$SCRIPT_DIR/../../etc" && pwd)
LIB_SHELL_DIR=$(cd "$ETC_DIR/lib/shell" && pwd)
INIT_KDC_SCRIPT="$ETC_DIR/init_kdc.sh"
if [[ ! -x "$INIT_KDC_SCRIPT" && -x "$SCRIPT_DIR/init_kdc.sh" ]]; then
INIT_KDC_SCRIPT="$SCRIPT_DIR/init_kdc.sh"
fi
ORIG_ARGS=("$@")
@ -530,8 +534,8 @@ run_test() {
# Try a fast availability wait; if it fails, treat as unhealthy and clean up before proceeding.
if ! kubectl -n "$kdc_ns" wait --for=condition=Available deploy/"$PROLE_KDC_NAME" --timeout="${PROLE_KDC_PRECHECK_TIMEOUT}s" >/dev/null 2>&1; then
echo "Detected unhealthy '$PROLE_KDC_NAME' deployment in namespace '$kdc_ns'; removing before Kerberos test rollout to avoid timeout..." >&2
if [[ -x "$SCRIPT_DIR/init_kdc.sh" ]]; then
SERVICE_NAMESPACE="$SERVICE_NAMESPACE" "$SCRIPT_DIR/init_kdc.sh" cleanup || true
if [[ -x "$INIT_KDC_SCRIPT" ]]; then
SERVICE_NAMESPACE="$SERVICE_NAMESPACE" "$INIT_KDC_SCRIPT" cleanup || true
else
kubectl -n "$kdc_ns" delete deploy "$PROLE_KDC_NAME" --ignore-not-found || true
kubectl -n "$kdc_ns" delete svc "$PROLE_KDC_SERVICE" --ignore-not-found || true
@ -540,10 +544,19 @@ run_test() {
fi
fi
if [[ -x "$SCRIPT_DIR/init_kdc.sh" ]]; then
SERVICE_NAMESPACE="$SERVICE_NAMESPACE" KRB5_REALM="$KRB5_REALM" KRB5_KDC="$KRB5_KDC" \
local kdc_mode="${PROLE_MODE:-${DEPLOYMENT_MODE:-}}"
local kdc_mode_args=()
if [[ -n "$kdc_mode" ]]; then
kdc_mode_args=(--mode "$kdc_mode")
fi
if [[ -x "$INIT_KDC_SCRIPT" ]]; then
if ! SERVICE_NAMESPACE="$SERVICE_NAMESPACE" KRB5_REALM="$KRB5_REALM" KRB5_KDC="$KRB5_KDC" \
KRB5_ADMIN="$KRB5_KDC" KRB5_USER="$KRB5_USER" KRB5_PASSWORD="$KRB5_PASSWORD" \
"$SCRIPT_DIR/init_kdc.sh" update || true
"$INIT_KDC_SCRIPT" "${kdc_mode_args[@]}" update; then
echo "ERROR: Failed to ensure KDC deployment '${PROLE_KDC_NAME}' in namespace '$kdc_ns'." >&2
exit 1
fi
fi
local pod_name

View File

@ -762,11 +762,21 @@ resolve_knoe_db_namespace() {
return 0
fi
if [[ -n "${DATABASE_NAMESPACE:-}" ]]; then
KNOE_DB_NAMESPACE="$DATABASE_NAMESPACE"
return 0
fi
if [[ -n "${NAMESPACE:-}" ]]; then
KNOE_DB_NAMESPACE="$NAMESPACE"
return 0
fi
if kubectl get namespace knoe-db >/dev/null 2>&1; then
KNOE_DB_NAMESPACE="knoe-db"
return 0
fi
if kubectl get namespace prole >/dev/null 2>&1; then
KNOE_DB_NAMESPACE="prole"
else
@ -1095,15 +1105,9 @@ setup_knoe_db_for_supabase() {
# Create roles required by Supabase services
kubectl -n "$ns" exec -i "$primary" -c postgres -- psql -U postgres -d postgres -c "
-- Core roles
-- Fix search_path for supabase_storage_admin so migration reads and writes use the same schema
-- Without this, the service queries auth.migrations (empty) but inserts into public.migrations
-- causing duplicate-key errors on every restart after the first successful migration run.
ALTER ROLE supabase_storage_admin SET search_path = storage, auth, public;
ALTER ROLE supabase_storage_admin BYPASSRLS;
-- Drop stale migrations tracking rows so the storage service re-runs cleanly (idempotent reinstall)
TRUNCATE TABLE IF EXISTS public.migrations;
-- Drop stale storage migration tracking tables so storage reinitializes cleanly.
-- Truncate is insufficient when prior failed starts left inconsistent duplicate rows.
DROP TABLE IF EXISTS public.migrations CASCADE;
DROP TABLE IF EXISTS storage.migrations CASCADE;
DO \$\$ BEGIN IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname='anon') THEN CREATE ROLE anon NOLOGIN; END IF; END \$\$;
@ -1111,20 +1115,35 @@ setup_knoe_db_for_supabase() {
DO \$\$ BEGIN IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname='service_role') THEN CREATE ROLE service_role NOLOGIN; END IF; END \$\$;
DO \$\$ BEGIN IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname='authenticator') THEN CREATE ROLE authenticator LOGIN PASSWORD '${pg_password}'; END IF; END \$\$;
DO \$\$ BEGIN IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname='pgbouncer') THEN CREATE ROLE pgbouncer LOGIN PASSWORD '${pg_password}'; END IF; END \$\$;
DO \$\$ BEGIN IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname='knoe') THEN CREATE ROLE knoe LOGIN PASSWORD '${pg_password}' SUPERUSER CREATEDB CREATEROLE; END IF; END \$\$;
DO \$\$ BEGIN IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname='supabase_admin') THEN CREATE ROLE supabase_admin LOGIN PASSWORD '${pg_password}' SUPERUSER; END IF; END \$\$;
DO \$\$ BEGIN IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname='supabase_auth_admin') THEN CREATE ROLE supabase_auth_admin LOGIN PASSWORD '${pg_password}' NOINHERIT; END IF; END \$\$;
DO \$\$ BEGIN IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname='supabase_storage_admin') THEN CREATE ROLE supabase_storage_admin LOGIN PASSWORD '${pg_password}' NOINHERIT; END IF; END \$\$;
DO \$\$ BEGIN IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname='supabase_functions_admin') THEN CREATE ROLE supabase_functions_admin LOGIN PASSWORD '${pg_password}' NOINHERIT; END IF; END \$\$;
DO \$\$ BEGIN IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname='supabase_read_only_user') THEN CREATE ROLE supabase_read_only_user NOLOGIN; END IF; END \$\$;
-- Fix search_path for supabase_storage_admin so migration reads and writes use the same schema.
-- Without this, the service queries auth.migrations (empty) but inserts into public.migrations,
-- causing duplicate-key errors on every restart after the first successful migration run.
ALTER ROLE supabase_storage_admin SET search_path = storage, auth, public;
ALTER ROLE supabase_storage_admin BYPASSRLS;
-- Always sync service role passwords to current postgres password (idempotent)
ALTER ROLE authenticator PASSWORD '${pg_password}';
ALTER ROLE pgbouncer PASSWORD '${pg_password}';
ALTER ROLE knoe WITH SUPERUSER CREATEDB CREATEROLE;
ALTER ROLE knoe PASSWORD '${pg_password}';
ALTER ROLE knoe SET search_path = knoe, public;
ALTER ROLE supabase_admin PASSWORD '${pg_password}';
ALTER ROLE supabase_auth_admin PASSWORD '${pg_password}';
ALTER ROLE supabase_storage_admin PASSWORD '${pg_password}';
ALTER ROLE supabase_functions_admin PASSWORD '${pg_password}';
-- External CNPG installs may lock down database-level CREATE by default.
-- Storage startup migrations run CREATE SCHEMA IF NOT EXISTS and fail without this.
GRANT CONNECT, TEMPORARY ON DATABASE postgres TO supabase_storage_admin;
GRANT CREATE ON DATABASE postgres TO supabase_storage_admin;
-- Grant membership
GRANT anon TO authenticator;
GRANT authenticated TO authenticator;
@ -1132,15 +1151,54 @@ setup_knoe_db_for_supabase() {
GRANT supabase_admin TO authenticator;
-- Schemas on postgres database
CREATE SCHEMA IF NOT EXISTS knoe AUTHORIZATION knoe;
CREATE SCHEMA IF NOT EXISTS auth AUTHORIZATION supabase_auth_admin;
CREATE SCHEMA IF NOT EXISTS storage AUTHORIZATION supabase_storage_admin;
CREATE SCHEMA IF NOT EXISTS graphql_public;
CREATE SCHEMA IF NOT EXISTS _realtime;
ALTER SCHEMA _realtime OWNER TO postgres;
GRANT USAGE, CREATE ON SCHEMA public TO supabase_storage_admin;
GRANT USAGE, CREATE ON SCHEMA knoe TO knoe;
GRANT USAGE ON SCHEMA public TO anon, authenticated, service_role;
GRANT USAGE ON SCHEMA auth TO anon, authenticated, service_role;
GRANT USAGE ON SCHEMA storage TO anon, authenticated, service_role;
GRANT USAGE ON SCHEMA graphql_public TO anon, authenticated, service_role;
-- Keep extension objects out of public schema to satisfy database linter checks.
DO \$\$
DECLARE
ext_name text;
BEGIN
FOREACH ext_name IN ARRAY ARRAY['pg_tde','pgcrypto','postgis','vector','tds_fdw']
LOOP
IF EXISTS (SELECT 1 FROM pg_extension WHERE extname = ext_name) THEN
BEGIN
EXECUTE format('ALTER EXTENSION %I SET SCHEMA knoe', ext_name);
EXCEPTION
WHEN OTHERS THEN
RAISE NOTICE 'Could not move extension % to schema knoe: %', ext_name, SQLERRM;
END;
END IF;
END LOOP;
END \$\$;
-- Resolve Supabase lint error: ensure RLS is enabled on public.spatial_ref_sys.
DO \$\$
BEGIN
IF to_regclass('public.spatial_ref_sys') IS NOT NULL THEN
ALTER TABLE public.spatial_ref_sys ENABLE ROW LEVEL SECURITY;
IF NOT EXISTS (
SELECT 1
FROM pg_policies
WHERE schemaname = 'public'
AND tablename = 'spatial_ref_sys'
AND policyname = 'spatial_ref_sys_select_all'
) THEN
CREATE POLICY spatial_ref_sys_select_all ON public.spatial_ref_sys
FOR SELECT USING (true);
END IF;
END IF;
END \$\$;
" 2>&1 || warn "Could not set up Supabase roles (cluster may not be ready yet)."
# Create _supabase database (used by supavisor, analytics)
@ -1154,6 +1212,8 @@ setup_knoe_db_for_supabase() {
# Create schemas in _supabase database
kubectl -n "$ns" exec "$primary" -c postgres -- psql -U postgres -d _supabase -c "
CREATE SCHEMA IF NOT EXISTS knoe AUTHORIZATION knoe;
GRANT USAGE, CREATE ON SCHEMA knoe TO knoe;
CREATE SCHEMA IF NOT EXISTS _supavisor;
ALTER SCHEMA _supavisor OWNER TO postgres;
CREATE SCHEMA IF NOT EXISTS _analytics;
@ -1259,8 +1319,8 @@ ensure_k8s_supabase_static_pvs() {
require_cmd kubectl
local node base parent base_name
node="${SUPABASE_PV_NODE:-merlin.prole.org}"
base="${SUPABASE_PV_BASE_DIR:-/synology/d002/supabase}"
node="${SUPABASE_PV_NODE:-gandalf.prole.org}"
base="${SUPABASE_PV_BASE_DIR:-${SUPABASE_PV_BASE:-/synology/d005/supabase}}"
base="${base%/}"
parent="$(dirname "$base")"
base_name="$(basename "$base")"
@ -1299,10 +1359,10 @@ spec:
BASE_DIR="/host-parent/${base_name}"
echo "[dirprep] node=${node} base=${base}"
mkdir -p "\${BASE_DIR}"
mkdir -p "\${BASE_DIR}/db" "\${BASE_DIR}/deno" "\${BASE_DIR}/functions" "\${BASE_DIR}/snippets"
mkdir -p "\${BASE_DIR}/db" "\${BASE_DIR}/deno" "\${BASE_DIR}/functions" "\${BASE_DIR}/snippets" "\${BASE_DIR}/imgproxy" "\${BASE_DIR}/minio" "\${BASE_DIR}/storage"
chmod 0777 "\${BASE_DIR}" 2>/dev/null || true
chmod 0777 "\${BASE_DIR}/db" "\${BASE_DIR}/deno" "\${BASE_DIR}/functions" "\${BASE_DIR}/snippets" 2>/dev/null || true
echo "[dirprep][OK] Prepared ${base}/{db,deno,functions,snippets}"
chmod 0777 "\${BASE_DIR}/db" "\${BASE_DIR}/deno" "\${BASE_DIR}/functions" "\${BASE_DIR}/snippets" "\${BASE_DIR}/imgproxy" "\${BASE_DIR}/minio" "\${BASE_DIR}/storage" 2>/dev/null || true
echo "[dirprep][OK] Prepared ${base}/{db,deno,functions,snippets,imgproxy,minio,storage}"
volumeMounts:
- name: host-parent
mountPath: /host-parent
@ -1330,20 +1390,41 @@ EOF
# Static local PVs with Retain reclaimPolicy can get stuck in Released after a namespace reset.
# Delete Released PV objects so they can be recreated and rebound to the new PVCs.
# imgproxy and storage PVs are no longer used (those services are removed).
local pvs=(
synology-supabase-db
synology-supabase-deno
synology-supabase-functions
synology-supabase-snippets
synology-supabase-imgproxy
synology-supabase-minio
synology-supabase-storage
)
local pv
for pv in "${pvs[@]}"; do
local phase
phase=$(kubectl get pv "$pv" -o jsonpath='{.status.phase}' 2>/dev/null || true)
local expected_path=""
case "$pv" in
synology-supabase-db) expected_path="${base}/db" ;;
synology-supabase-deno) expected_path="${base}/deno" ;;
synology-supabase-functions) expected_path="${base}/functions" ;;
synology-supabase-snippets) expected_path="${base}/snippets" ;;
synology-supabase-imgproxy) expected_path="${base}/imgproxy" ;;
synology-supabase-minio) expected_path="${base}/minio" ;;
synology-supabase-storage) expected_path="${base}/storage" ;;
esac
local current_path current_node
current_path=$(kubectl get pv "$pv" -o jsonpath='{.spec.local.path}' 2>/dev/null || true)
current_node=$(kubectl get pv "$pv" -o jsonpath='{.spec.nodeAffinity.required.nodeSelectorTerms[0].matchExpressions[0].values[0]}' 2>/dev/null || true)
if [[ "${phase:-}" == "Released" ]]; then
warn "PV '$pv' is in Released state; deleting PV object to allow rebind"
kubectl delete pv "$pv" --ignore-not-found >/dev/null 2>&1 || true
elif [[ -n "$current_path" && -n "$expected_path" && "$current_path" != "$expected_path" ]]; then
warn "PV '$pv' path mismatch (current=${current_path}, expected=${expected_path}); recreating PV object"
kubectl delete pv "$pv" --ignore-not-found >/dev/null 2>&1 || true
elif [[ -n "$current_node" && "$current_node" != "$node" ]]; then
warn "PV '$pv' node mismatch (current=${current_node}, expected=${node}); recreating PV object"
kubectl delete pv "$pv" --ignore-not-found >/dev/null 2>&1 || true
fi
done
@ -1448,9 +1529,104 @@ spec:
operator: In
values:
- ${node}
---
apiVersion: v1
kind: PersistentVolume
metadata:
name: synology-supabase-imgproxy
labels:
synology.storage/pvc: supabase-imgproxy
spec:
capacity:
storage: 1Gi
volumeMode: Filesystem
accessModes:
- ReadWriteOnce
storageClassName: ${storage_class}
persistentVolumeReclaimPolicy: Retain
local:
path: ${base}/imgproxy
nodeAffinity:
required:
nodeSelectorTerms:
- matchExpressions:
- key: kubernetes.io/hostname
operator: In
values:
- ${node}
---
apiVersion: v1
kind: PersistentVolume
metadata:
name: synology-supabase-minio
labels:
synology.storage/pvc: supabase-minio
spec:
capacity:
storage: 1Gi
volumeMode: Filesystem
accessModes:
- ReadWriteOnce
storageClassName: ${storage_class}
persistentVolumeReclaimPolicy: Retain
local:
path: ${base}/minio
nodeAffinity:
required:
nodeSelectorTerms:
- matchExpressions:
- key: kubernetes.io/hostname
operator: In
values:
- ${node}
---
apiVersion: v1
kind: PersistentVolume
metadata:
name: synology-supabase-storage
labels:
synology.storage/pvc: supabase-storage
spec:
capacity:
storage: 1Gi
volumeMode: Filesystem
accessModes:
- ReadWriteOnce
storageClassName: ${storage_class}
persistentVolumeReclaimPolicy: Retain
local:
path: ${base}/storage
nodeAffinity:
required:
nodeSelectorTerms:
- matchExpressions:
- key: kubernetes.io/hostname
operator: In
values:
- ${node}
EOF
}
enforce_supabase_workload_node() {
local ns="$1"
local node
node="${SUPABASE_NODE_SELECTOR:-${SUPABASE_PV_NODE:-}}"
if [[ -z "${node:-}" ]]; then
return 0
fi
log "Forcing Supabase workloads onto node '${node}'..."
local kind resource
for kind in deployment statefulset; do
while IFS= read -r resource; do
[[ -n "$resource" ]] || continue
kubectl -n "$ns" patch "$resource" --type merge -p "{\"spec\":{\"template\":{\"spec\":{\"nodeSelector\":{\"kubernetes.io/hostname\":\"${node}\"}}}}}" >/dev/null 2>&1 || \
warn "Could not patch ${resource} with nodeSelector ${node}"
done < <(kubectl -n "$ns" get "$kind" -o name 2>/dev/null || true)
done
}
run_helm() {
if [[ "$MODE" == "local" ]]; then
return 1
@ -1504,6 +1680,30 @@ print(c.get('Global','SUPABASE_STORAGE_CLASS',fallback=''))" 2>/dev/null || true
if [[ -z "$storage_class" && "$MODE" == "k8s" ]]; then
storage_class="synology-iscsi"
fi
if [[ -z "${SUPABASE_PV_NODE:-}" && -n "${PROLE_CFG_PATH:-}" ]]; then
SUPABASE_PV_NODE=$(python3 -c "
import configparser
c=configparser.ConfigParser(); c.read('${PROLE_CFG_PATH}')
print(c.get('Global','SUPABASE_PV_NODE',fallback=''))" 2>/dev/null || true)
fi
if [[ -z "${SUPABASE_PV_BASE_DIR:-}" && -n "${PROLE_CFG_PATH:-}" ]]; then
SUPABASE_PV_BASE_DIR=$(python3 -c "
import configparser
c=configparser.ConfigParser(); c.read('${PROLE_CFG_PATH}')
base=c.get('Global','SUPABASE_PV_BASE_DIR',fallback='').strip() or c.get('Global','SUPABASE_PV_BASE',fallback='').strip()
print(base)" 2>/dev/null || true)
fi
if [[ -n "${SUPABASE_PV_BASE_DIR:-}" && -z "${SUPABASE_PV_BASE:-}" ]]; then
SUPABASE_PV_BASE="$SUPABASE_PV_BASE_DIR"
fi
if [[ -n "${SUPABASE_PV_NODE:-}" ]]; then
export SUPABASE_PV_NODE
fi
if [[ -n "${SUPABASE_PV_BASE_DIR:-}" ]]; then
export SUPABASE_PV_BASE_DIR
export SUPABASE_PV_BASE
fi
# Guard: refuse non-synology storage classes to prevent SD-card crash on pi
if [[ -n "$storage_class" ]] && ! [[ "$storage_class" =~ ^(synology|merlin-local-iscsi|myrddin-local-iscsi) ]]; then
die "SUPABASE_STORAGE_CLASS '${storage_class}' is not a synology/iSCSI mount. Refusing Supabase deploy to prevent node crash."
@ -1518,12 +1718,15 @@ print(c.get('Global','SUPABASE_STORAGE_CLASS',fallback=''))" 2>/dev/null || true
local helm_set_args=()
if [[ -n "$storage_class" ]]; then
# Only set storageClass for components still deployed; storage/minio/imgproxy are removed.
# Keep all non-DB Supabase PVC-backed components on the selected storage class.
helm_set_args+=(
--set "persistence.db.storageClassName=${storage_class}"
--set "persistence.deno.storageClassName=${storage_class}"
--set "persistence.functions.storageClassName=${storage_class}"
--set "persistence.imgproxy.storageClassName=${storage_class}"
--set "persistence.minio.storageClassName=${storage_class}"
--set "persistence.snippets.storageClassName=${storage_class}"
--set "persistence.storage.storageClassName=${storage_class}"
)
fi
@ -1555,6 +1758,8 @@ print(c.get('Global','SUPABASE_STORAGE_CLASS',fallback=''))" 2>/dev/null || true
return 1
fi
enforce_supabase_workload_node "$ns"
local wait_enabled
wait_enabled="${SUPABASE_DEPLOY_WAIT:-true}"
if [[ "$wait_enabled" == "true" ]]; then

View File

@ -180,6 +180,57 @@ def test_main_rejects_repair_and_update_together(tmp_path, monkeypatch):
screens_mod.main()
def test_main_rejects_delete_db_without_reset(tmp_path, monkeypatch):
"""CLI must require --reset when --delete-db is provided."""
import knoe.ui.screens as screens_mod
cfg_path = tmp_path / "prole.cfg"
_write_cfg(cfg_path, "[Inputs]\n[Global]\n")
monkeypatch.setattr(
sys,
"argv",
["install.py", "--silent", "--delete-db", "-c", str(cfg_path)],
)
with pytest.raises(SystemExit):
screens_mod.main()
def test_main_delete_db_is_forwarded_to_console_installer(tmp_path, monkeypatch):
"""install.py --delete-db must be forwarded to ProleConsoleInstaller in silent mode."""
import knoe.ui.screens as screens_mod
cfg_path = tmp_path / "prole.cfg"
_write_cfg(cfg_path, "[Inputs]\n[Global]\n")
installer = MagicMock()
installer.run.return_value = 0
installer.run_update.return_value = 0
installer.run_repair.return_value = 0
installer_factory = MagicMock(return_value=installer)
monkeypatch.setattr(
sys,
"argv",
["install.py", "--silent", "--reset", "--delete-db", "-c", str(cfg_path)],
)
monkeypatch.setattr(screens_mod, "_ensure_ansible_vault_credentials", lambda **_: None)
monkeypatch.setattr(screens_mod, "ProleController", MagicMock(return_value=MagicMock()))
monkeypatch.setattr(screens_mod, "ProleConsoleInstaller", installer_factory)
with pytest.raises(SystemExit) as ex:
screens_mod.main()
assert ex.value.code == 0
installer_factory.assert_called_once()
_, kwargs = installer_factory.call_args
assert kwargs.get("reset_cluster") is True
assert kwargs.get("delete_db") is True
installer.run.assert_called_once()
def test_repair_flag_does_not_trigger_silent_install(tmp_path):
"""--repair must dispatch to run_repair(), NOT run() (full install)."""
from knoe.core.actions import ProleConsoleInstaller
@ -485,3 +536,56 @@ def test_reconcile_supabase_calls_studio_when_enabled(tmp_path):
assert studio_reconcile_called, (
"_reconcile_supabase_studio() must be called when studio is enabled"
)
def test_supabase_deploy_includes_knoe_schema_and_lint_remediation_sql():
"""supabase/deploy.sh must include recurring knoe-schema and lint remediation SQL."""
script = (REPO_ROOT / "supabase" / "deploy.sh").read_text(encoding="utf-8")
assert "CREATE ROLE knoe LOGIN PASSWORD" in script
assert "CREATE SCHEMA IF NOT EXISTS knoe AUTHORIZATION knoe;" in script
assert "DROP TABLE IF EXISTS public.migrations CASCADE;" in script
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
def test_reset_k3s_namespace_delete_db_deletes_pvcs_and_bound_pvs(tmp_path, monkeypatch):
"""DB delete reset must request PVC deletion and force cleanup of bound PVs."""
import subprocess as sp
from knoe.core import actions as actions_mod
script = tmp_path / "scripts" / "reset-ns.sh"
script.parent.mkdir(parents=True, exist_ok=True)
script.write_text("#!/usr/bin/env bash\nexit 0\n", encoding="utf-8")
calls: list[list[str]] = []
def _fake_run(cmd, **kwargs):
calls.append(list(cmd))
if cmd[:6] == ["kubectl", "-n", "knoe-db", "get", "pvc", "-o"]:
return sp.CompletedProcess(cmd, 0, stdout="pv-a\npv-b\n", stderr="")
return sp.CompletedProcess(cmd, 0, stdout="", stderr="")
monkeypatch.setattr(actions_mod.subprocess, "run", _fake_run)
actions_mod._reset_k3s_namespace(
tmp_path,
["knoe-db"],
"",
"",
delete_pvcs_namespaces=["knoe-db"],
force_delete_bound_pvs=True,
)
assert any(
cmd[:3] == ["bash", str(script), "-n"]
and "--delete-pvcs" in cmd
for cmd in calls
)
assert ["kubectl", "patch", "pv", "pv-a", "--type=merge", "-p", '{"spec":{"persistentVolumeReclaimPolicy":"Delete"}}'] in calls
assert ["kubectl", "patch", "pv", "pv-b", "--type=merge", "-p", '{"spec":{"persistentVolumeReclaimPolicy":"Delete"}}'] in calls
assert ["kubectl", "delete", "pv", "pv-a", "--ignore-not-found", "--wait=false"] in calls
assert ["kubectl", "delete", "pv", "pv-b", "--ignore-not-found", "--wait=false"] in calls