Align installer namespace/topology handling and add regression tests

Co-authored-by: Junie <junie@jetbrains.com>
This commit is contained in:
chrisfu 2026-03-29 23:08:14 -07:00
parent 4164cf9f87
commit 22420408f9
20 changed files with 635 additions and 165 deletions

View File

@ -7,6 +7,7 @@
"cluster_name": "knoe-db",
"desired_instances": 3,
"eligible_nodes": [
"gandalf.prole.org",
"merlin.prole.org",
"myrddin.prole.org",
"pi.prole.org"
@ -17,7 +18,7 @@
"regenerated": false,
"reused": true
},
"plan_hash": "10dccbb8fe133be4",
"plan_id": "cnpg-placement-10dccbb8fe133be4",
"plan_hash": "7036c5f65b6ef0db",
"plan_id": "cnpg-placement-7036c5f65b6ef0db",
"schema_version": "v1"
}

View File

@ -6,6 +6,6 @@ garage: local=3900 remote=3900 ns=knoe-system svc=garage address=0.0.0.0
openbao: local=8200 remote=8200 ns=knoe-system svc=openbao address=0.0.0.0
opentofu: local=8080 remote=8080 ns=knoe-system svc=opentofu address=0.0.0.0
dashboard: local=8443 remote=443 ns=kubernetes-dashboard svc=kubernetes-dashboard-kong-proxy address=127.0.0.1
postgres: local=5432 remote=5432 ns=knoe-db-17-7-043-18-140-17-7-043-18-140-17-7-043-18-140 svc=knoe-db-rw address=0.0.0.0
postgres: local=5432 remote=5432 ns=knoe-db svc=knoe-db-rw address=0.0.0.0
prometheus: local=9090 remote=9090 ns=monitoring svc=kps-kube-prometheus-stack-prometheus address=127.0.0.1
grafana: local=3000 remote=80 ns=monitoring svc=kps-grafana address=0.0.0.0

View File

@ -1,5 +1,5 @@
; Prole Master Configuration File
; Generated by install.py on 2026-03-29 19:03:07
; Generated by install.py on 2026-03-29 22:54:52
; This file is used as input for Ansible deployment and k8s cluster creation.
[User]
@ -53,32 +53,25 @@ init_cluster.cluster_env = service
init_cluster.deployment_target = prole-service-cluster
init_cluster.gitops_enabled = false
init_cluster.k3s_server_url = https://myrddin.prole.org:6443
init_cluster.k3s_token = ${PROLE_SECRET:v1:HI4QqnXC0_CddPHY:xg1q0N70GVGDMn588uF15I3w39t3kjJVhEDxYyGNLluTwYK7VI8BsOJY9t5BnSbTY7EgYXW6NhvlV4djJ1dHQ4k2NjvjkEjwS3f8fgCyiv449RnDARCoAy6Y5_uXm9Ci5lkpjk6oLjmp_-i3WuXO8Ybj-9qIWwKe1ZeJxg==}
init_cluster.k3s_token = ${PROLE_SECRET:v1:ncw40j3021elkcqU:OO4QXoXGTi4ptxeNvqFV9WXNsh_f2QEqSYnJxIR-MW-0J50oItM9NLQeQT3rY2Hju2C2VlSyeuKi3-MBGsm3wzQl5m1yjuVQ9MKrT20u2_pjyom3GuIhfCEx6agfadRHH_9lAio3wTVdvZqBgrTRyMQ67Um07eZRsXBTUA==}
init_cluster.kerberos_enabled = true
init_cluster.mode = k3s
init_cluster.start_cluster = true
init_cluster.supabase_analytics_enabled = true
init_cluster.supabase_auth_enabled = true
init_cluster.supabase_enabled = false
init_cluster.supabase_meta_enabled = true
init_cluster.supabase_realtime_enabled = true
init_cluster.supabase_studio_enabled = false
init_cluster.supabase_studio_url = db.prole.org
init_cnpg_deploy.force_rollout = false
init_cnpg_deploy.run_deploy = true
init_db_build.run_build = true
init_password.cluster_name = ${CLUSTER_NAME}
init_password.db_host_port = 5432
init_password.db_namespace = ${DATABASE_NAMESPACE}
init_password.db_password = ${PROLE_SECRET:v1:PjGWbznR1azeYyuX:Aga3J66GBbGlb1d8QxX1eGR_Gtqw_lPiFu23RH4cNldXrL-S2lCsYf5_iFbYvZB_}
init_password.db_password_confirm = ${PROLE_SECRET:v1:PjGWbznR1azeYyuX:Aga3J66GBbGlb1d8QxX1eGR_Gtqw_lPiFu23RH4cNldXrL-S2lCsYf5_iFbYvZB_}
init_password.db_password = ${PROLE_SECRET:v1:4QVLv6L_Zb6w1Zja:4PHGYWVndekQb3ZG7hjNAoIQ_T2yEZlS}
init_password.db_password_confirm = ${PROLE_SECRET:v1:4QVLv6L_Zb6w1Zja:4PHGYWVndekQb3ZG7hjNAoIQ_T2yEZlS}
init_password.db_username = root
init_password.generate_ssh_key = true
init_scripts.run_scripts = true
kerberos_config.enabled = true
kerberos_config.init_authority = false
kerberos_config.kdc = 10.0.0.3
kerberos_config.password = ${PROLE_SECRET:v1:vcWmYfhC0O42zIJa:Lt20P_X9KZLN-HC--wv0E48d4vEiH3z4B8rRCNjb7-RV9Ijx}
kerberos_config.password = ${PROLE_SECRET:v1:lDmjQ4Cj45LYBD9_:9Aes8WNTZ97oYpi1aFvS8ODHDCQJJJCUBFgNrJBZ3a-Mgwp2}
kerberos_config.realm = PROLE.ORG
kerberos_config.test_connection = false
kerberos_config.user = administrator
@ -92,14 +85,14 @@ ollama_config.server_port = 11434
ARGOCD_NAMESPACE = argocd
CLUSTER_ENV = service
CLUSTER_NAME = knoe-db
CNPG_ELIGIBLE_NODES = merlin.prole.org,myrddin.prole.org,pi.prole.org
CNPG_ELIGIBLE_NODES = gandalf.prole.org,merlin.prole.org,myrddin.prole.org,pi.prole.org
CNPG_PLACEMENT_PLAN_FILE = /Users/chrisfu/dev/prole/conf/cnpg-placement/knoe-system-knoe-db.json
CNPG_PLACEMENT_PLAN_HASH = 10dccbb8fe133be4
CNPG_PLACEMENT_PLAN_ID = cnpg-placement-10dccbb8fe133be4
CNPG_STAGE1_NODE = merlin.prole.org
DATABASE_NAMESPACE = knoe-db-17-7-043-18-140-17-7-043-18-140-17-7-043-18-140
CNPG_PLACEMENT_PLAN_HASH = 7036c5f65b6ef0db
CNPG_PLACEMENT_PLAN_ID = cnpg-placement-7036c5f65b6ef0db
CNPG_STAGE1_NODE = gandalf.prole.org
DATABASE_NAMESPACE = knoe-db
DB_HOST_PORT = 5432
DB_PASSWORD = ${PROLE_SECRET:v1:JKAbSEfmoY7qCzGW:Kin_0B_SV7A5RDyNYfXFi43jbcI_DOe6UwCbNuLMQ67h6tVXef5Wui-ErAW7JEoJ}
DB_PASSWORD = ${PROLE_SECRET:v1:FgbkzVICzbA4WX7l:t5eAnQUYrTfAiXuUUPgZ8rBJVqAp-WFA}
DEPLOYMENT_MODE = k3s
DEPLOYMENT_TARGET = prole-service-cluster
DOCKER_PRELOAD = false
@ -107,10 +100,9 @@ KNOE_DB_USER = root
KUBECONTEXT = knoe.dev.prole.org
OPTIONAL_WORKLOADS_MIN_READY_SCHEDULABLE_NODES = 2
PROLE_K3S_SERVER = https://myrddin.prole.org:6443
PROLE_K3S_TOKEN = K107c8c6000488eca4a067d8a73119bbae2f07b4ea1bac7d8d3dc9c500cbb8acb18::server:04572345810eae2f9619a6ed4239702b
REGISTRY_NAMESPACE = knoe-system
PROLE_K3S_TOKEN = ${PROLE_SECRET:v1:ncw40j3021elkcqU:OO4QXoXGTi4ptxeNvqFV9WXNsh_f2QEqSYnJxIR-MW-0J50oItM9NLQeQT3rY2Hju2C2VlSyeuKi3-MBGsm3wzQl5m1yjuVQ9MKrT20u2_pjyom3GuIhfCEx6agfadRHH_9lAio3wTVdvZqBgrTRyMQ67Um07eZRsXBTUA==}
SERVICE_NAMESPACE = knoe-system
SYNOLOGY_ROOTS = /synology/d001,/synology/d002,/synology/d003,/synology/d004
SYNOLOGY_ROOTS = /synology/d001,/synology/d002,/synology/d003,/synology/d004,/synology/d005
[Welcome]
; No configuration values captured yet for this section.
@ -131,14 +123,7 @@ KDC_AUTO_DETECTED = 10.0.0.3
KERBEROS_AUTO_ENABLED = True
[Port Forwards]
PORT_FORWARD_K3S_MAPPING_1 = id=argocd;namespace=argocd;target=svc/argocd-server;address=0.0.0.0;hostPort=8081;servicePort=80;protocol=TCP;description=ArgoCD
PORT_FORWARD_K3S_MAPPING_2 = id=garage;namespace=knoe-system;target=svc/garage;address=0.0.0.0;hostPort=3900;servicePort=3900;protocol=TCP;description=Garage S3
PORT_FORWARD_K3S_MAPPING_3 = id=openbao;namespace=knoe-system;target=svc/openbao;address=0.0.0.0;hostPort=8200;servicePort=8200;protocol=TCP;description=OpenBao
PORT_FORWARD_K3S_MAPPING_4 = id=opentofu;namespace=knoe-system;target=svc/opentofu;address=0.0.0.0;hostPort=8080;servicePort=8080;protocol=TCP;description=OpenTofu
PORT_FORWARD_K3S_MAPPING_5 = id=dashboard;namespace=kubernetes-dashboard;target=svc/kubernetes-dashboard-kong-proxy;address=127.0.0.1;hostPort=8443;servicePort=443;protocol=TCP;description=Kubernetes Dashboard
PORT_FORWARD_K3S_MAPPING_6 = id=postgres;namespace=${DATABASE_NAMESPACE};target=svc/knoe-db-rw;address=0.0.0.0;hostPort=5432;servicePort=5432;protocol=TCP;description=PostgreSQL (primary)
PORT_FORWARD_K3S_MAPPING_7 = id=prometheus;namespace=monitoring;target=svc/kps-kube-prometheus-stack-prometheus;address=127.0.0.1;hostPort=9090;servicePort=9090;protocol=TCP;description=Prometheus UI
PORT_FORWARD_K3S_MAPPING_8 = id=grafana;namespace=monitoring;target=svc/kps-grafana;address=0.0.0.0;hostPort=3000;servicePort=80;protocol=TCP;description=Grafana UI
; No configuration values captured yet for this section.
[System Environment]
PROLE_CONF = /Users/chrisfu/dev/prole/conf
@ -159,12 +144,13 @@ OLLAMA_SERVER_HOST = fairyland.prole.org
OLLAMA_SERVER_PORT = 11434
[Optional Features]
AT_REST_ENCRYPTION_ENABLED = true
GITOPS_ENABLED = false
KERBEROS_ENABLED = true
SUPABASE_ENABLED = true
AT_REST_ENCRYPTION_ENABLED = True
GITOPS_ENABLED = False
KERBEROS_ENABLED = True
SUPABASE_ENABLED = False
[GitOps]
; No configuration values captured yet for this section.
[Database Creation]
; No configuration values captured yet for this section.
@ -172,34 +158,32 @@ SUPABASE_ENABLED = true
[Initialize Cluster]
ENVIRONMENT = service
K3S_SERVER_URL = https://myrddin.prole.org:6443
K3S_TOKEN = ${PROLE_SECRET:v1:k9baSCELlyPkN1nT:l45ZjmSD0i6jjG9WgAEAyqKIjvRY5RdVEvRAWmMDCTwAhWAv6vOqW3QTp98MjGlixQR9r2fD-DYg42w9BlBltiXZpBKy3g16YS5NxCq-yxU0FLtimuujreKnUuX0QQymOgyQsZD3co_zWoxwAaB0SfpxVoO2XbC6lxn1MQ==}
K3S_TOKEN = ${PROLE_SECRET:v1:ncw40j3021elkcqU:OO4QXoXGTi4ptxeNvqFV9WXNsh_f2QEqSYnJxIR-MW-0J50oItM9NLQeQT3rY2Hju2C2VlSyeuKi3-MBGsm3wzQl5m1yjuVQ9MKrT20u2_pjyom3GuIhfCEx6agfadRHH_9lAio3wTVdvZqBgrTRyMQ67Um07eZRsXBTUA==}
[Dev Cluster (k3d)]
CLUSTER_ENV = k3d-knoe-dev-cluster
CLUSTER_ENV = dev
DISPLAY_NAME = knoe-dev-cluster
KUBECTL_CONTEXT = service
KUBECTL_CONTEXT = knoe.dev.prole.org
MODE = k3d
[Service Cluster (k3s)]
CLUSTER_ENV = prole-service-cluster
DISPLAY_NAME = prole-service-cluster
K3S_SERVER_URL = https://myrddin.prole.org:6443
K3S_TOKEN = ${PROLE_SECRET:v1:Qt-K_U5RwLwTiQt9:FkC_VDt1A-NStDuWxzxrZCh5Ebo_WGSEef8CA8OUHcGUg3sPDYYmNsscNlBgD0lnEmgWapix7ZdEgRc0pe6d7grZhCrfg-Fe-5CPeISOWvgUVOedyl6MfAZPciD95aI47NKayoElYFx8dUOb4durP7yfh_2t8zj3rW2O1A==}
K3S_TOKEN = ${PROLE_SECRET:v1:ncw40j3021elkcqU:OO4QXoXGTi4ptxeNvqFV9WXNsh_f2QEqSYnJxIR-MW-0J50oItM9NLQeQT3rY2Hju2C2VlSyeuKi3-MBGsm3wzQl5m1yjuVQ9MKrT20u2_pjyom3GuIhfCEx6agfadRHH_9lAio3wTVdvZqBgrTRyMQ67Um07eZRsXBTUA==}
MODE = k3s
PIPELINE_URL = http://myrddin.prole.org:8080
[Prod Cluster (k8s)]
ARTIFACTS_DIR =
ARTIFACTS_DIR = /Users/chrisfu/dev/prole/data/staging
CLUSTER_ENV = prole-prod-cluster
DISPLAY_NAME = prole-prod-cluster
MODE = k8s
PIPELINE_URL = http://myrddin.prole.org:8080
[Docker Build]
; No configuration values captured yet for this section.
[Initialization Scripts]
; No configuration values captured yet for this section.
STATUS = Attempted
[Deployment]
MODE = k3s

View File

@ -257,7 +257,6 @@ STATUS_SCRIPTS=(
"init_cloudnative_pg.sh"
"init_openbao.sh"
"init_kong.sh"
"init_db_manager.sh"
"init_monitoring.sh"
"init_cnpg_backup.sh"
"init_port_forwards.sh"

View File

@ -463,8 +463,6 @@ def _mount_rows():
def _fs_stats(mountpoint: str):
host_mount = Path("/host") / mountpoint.lstrip("/")
if not host_mount.exists():
return None, None, None
try:
st = os.statvfs(str(host_mount))
size = int(st.f_frsize * st.f_blocks)

View File

@ -844,6 +844,12 @@ class ClusterScreenMixin:
return "dev"
return key
def _is_valid_k8s_namespace(self, value: str | None) -> bool:
ns = (value or "").strip()
if not ns or len(ns) > 63:
return False
return re.fullmatch(r"[a-z0-9]([-a-z0-9]*[a-z0-9])?", ns) is not None
def _get_service_namespace(self) -> str:
"""Resolve service namespace with UI/env/config fallbacks.
@ -852,23 +858,25 @@ class ClusterScreenMixin:
"""
# 1) Explicit user/config/env choice wins.
candidate_namespaces: list[str] = []
try:
ns = (self.service_namespace.get() or "").strip()
candidate_namespaces.append((self.service_namespace.get() or "").strip())
except Exception:
ns = ""
if not ns:
try:
ns = (
pass
try:
candidate_namespaces.append(
(
(self.prole_cfg_data.get("Global", {}) or {})
.get("SERVICE_NAMESPACE", "")
.strip()
)
except Exception:
ns = ""
if not ns:
ns = (os.environ.get("SERVICE_NAMESPACE") or "").strip()
if ns:
return ns
)
except Exception:
pass
candidate_namespaces.append((os.environ.get("SERVICE_NAMESPACE") or "").strip())
for ns in candidate_namespaces:
if self._is_valid_k8s_namespace(ns):
return ns
# 2) If the cluster is reachable, infer the common services namespace.
inferred = ""
@ -876,7 +884,7 @@ class ClusterScreenMixin:
inferred = (self._infer_common_services_namespace() or "").strip()
except Exception:
inferred = ""
if inferred:
if self._is_valid_k8s_namespace(inferred):
return inferred
# 3) Stable defaults for known environments.

View File

@ -254,8 +254,10 @@ class ClusterNodesScreenMixin:
def _cfg(_evt=None):
try:
req_w = table.winfo_reqwidth()
canvas_w = canvas.winfo_width()
canvas.itemconfigure(inner, width=max(canvas_w, req_w))
canvas.configure(scrollregion=canvas.bbox("all"))
canvas.itemconfigure(inner, height=canvas.winfo_height())
except Exception:
pass
@ -527,20 +529,28 @@ class ClusterNodesScreenMixin:
self._overlay_widgets.append(msg_frame)
msg_win = self.bg_canvas.create_window(48, 292, window=msg_frame, anchor="nw", width=905, height=86)
self._canvas_items.append(msg_win)
msg_text = "\n".join(messages[-4:]) if messages else "Starting topology discovery"
msg_lbl = tk.Label(
msg_text = "\n".join(messages) if messages else "Starting topology discovery"
msg_scroll = tk.Scrollbar(msg_frame, orient="vertical")
msg_scroll.pack(side="right", fill="y")
msg_out = tk.Text(
msg_frame,
text=msg_text,
bg="#f6f8fb",
fg="#1d1d1f",
justify="left",
anchor="w",
font=("SF Pro Text", 10),
padx=10,
pady=8,
relief="flat",
highlightthickness=0,
bd=0,
wrap="word",
yscrollcommand=msg_scroll.set,
)
msg_lbl.pack(fill="both", expand=True)
self._overlay_widgets.append(msg_lbl)
msg_out.pack(side="left", fill="both", expand=True)
msg_scroll.configure(command=msg_out.yview)
msg_out.insert("1.0", msg_text)
msg_out.see("end")
msg_out.configure(state="disabled")
self._overlay_widgets.extend([msg_scroll, msg_out])
topology = state.get("topology") if isinstance(state.get("topology"), ClusterTopology) else None
y = self._cluster_nodes_render_summary_cards(topology, 48, 388)

View File

@ -402,6 +402,9 @@ class DatabaseOptionsScreenMixin:
elif ext_id == "pg_cron":
# pg_cron is bundled with Percona PG packages in this build flow.
pkg = ""
elif ext_id == "pg_tde":
# pg_tde uses Percona-specific package naming handled separately in templates.
pkg = ""
elif ext_id == "pgbadger":
pkg = "percona-pgbadger"
elif ext_id == "pg_stat_monitor":

View File

@ -175,7 +175,6 @@ class ServicesScreenMixin:
]
scripts.append(("Prole DB Backup", "init_cnpg_backup.sh"))
scripts.append(("Kong API Gateway", "init_kong.sh"))
scripts.append(("DB Manager", "init_db_manager.sh"))
scripts.append(("Monitoring", "init_monitoring.sh"))
mode = self._deployment_mode()
if mode != "k3d":
@ -228,8 +227,19 @@ class ServicesScreenMixin:
env["KNOE_DB_USER"] = self.db_username.get()
env["DB_PASSWORD"] = password
env["GRAFANA_ADMIN_PASSWORD"] = password
env["NAMESPACE"] = (self.db_namespace.get() or "").strip()
env["PROLE_NAMESPACE"] = env["NAMESPACE"]
db_namespace = (self.db_namespace.get() or "").strip()
if not db_namespace:
db_namespace = (
((getattr(self, "prole_cfg_data", {}) or {}).get("Global", {}) or {})
.get("DATABASE_NAMESPACE", "")
.strip()
)
if not db_namespace:
db_namespace = "default"
env["DATABASE_NAMESPACE"] = db_namespace
env["NAMESPACE"] = db_namespace
env["PROLE_NAMESPACE"] = db_namespace
env["SERVICE_NAMESPACE"] = self._get_service_namespace()
if self.kerberos_realm.get().strip():
env["KRB5_REALM"] = self.kerberos_realm.get().strip()
@ -325,7 +335,11 @@ class ServicesScreenMixin:
pass
rc_svc = 0
svc_namespace = env.get("NAMESPACE") or env.get("SERVICE_NAMESPACE") or "default"
svc_namespace = (
env.get("SERVICE_NAMESPACE")
or env.get("NAMESPACE")
or "default"
)
registry_ns = env.get("REGISTRY_NAMESPACE") or svc_namespace
project_root = getattr(self.controller, "project_root", PROJECT_ROOT)
@ -542,56 +556,7 @@ class ServicesScreenMixin:
"Skipping Kong because previous steps failed.\n"
)
# 6. init_db_manager.sh start
if overall_success:
script = "init_db_manager.sh"
if script in self.script_consoles:
_select_tab(script)
self.script_consoles[script].clear()
self.script_consoles[script].write(f"Running {script} start...\n")
log_path = _log_path_for(script)
self._record_install_log(log_path)
try:
log_fp = log_path.open("a", encoding="utf-8")
except Exception:
log_fp = None
def _dbmgr_line(line):
self.script_consoles[script].write(line)
self._process_script_output_line(line)
if log_fp:
try:
log_fp.write(line)
log_fp.flush()
except Exception:
pass
rc_dbmgr = self.controller.run_script(
script, args=mode_args + ["start"], env=env, on_line=_dbmgr_line
)
if log_fp:
try:
log_fp.close()
except Exception:
pass
if rc_dbmgr != 0:
self.script_consoles[script].write(
f"\nERROR: {script} start failed with code {rc_dbmgr}\n"
)
overall_success = False
else:
self.script_consoles[script].write(
f"\n{script} completed successfully.\n"
)
else:
if "init_db_manager.sh" in self.script_consoles:
self.script_consoles["init_db_manager.sh"].write(
"Skipping DB Manager because previous steps failed.\n"
)
# 7. init_monitoring.sh initialize
# 6. init_monitoring.sh initialize
if overall_success and opt_allowed:
script = "init_monitoring.sh"
_select_tab(script)
@ -688,7 +653,7 @@ class ServicesScreenMixin:
"Skipping Monitoring initialization because previous steps failed.\n"
)
# 8. init_nginx_ingress.sh initialize (k3s only — not used in k3d)
# 7. init_nginx_ingress.sh initialize (k3s only — not used in k3d)
if overall_success and mode != "k3d":
script = "init_nginx_ingress.sh"
_select_tab(script)
@ -736,7 +701,7 @@ class ServicesScreenMixin:
"Skipping Nginx Ingress because previous steps failed.\n"
)
# 9. init_port_forwards.sh start (k3d only — not used in k3s)
# 8. init_port_forwards.sh start (k3d only — not used in k3s)
if overall_success and mode != "k3s":
script = "init_port_forwards.sh"
if script in self.script_consoles:
@ -799,7 +764,10 @@ class ServicesScreenMixin:
)
# Verify critical secrets
ns = env.get("NAMESPACE", "default")
ns = (
str(env.get("DATABASE_NAMESPACE") or env.get("NAMESPACE") or "default").strip()
or "default"
)
self.script_consoles["init_cloudnative_pg.sh"].write(
f"\nVerifying critical secrets in namespace {ns}...\n"
)

View File

@ -1 +1 @@
145
150

View File

@ -1215,18 +1215,15 @@ PY
# 6. Kong API Gateway
run_init_script init_kong.sh -n "${NAMESPACE:-knoe-db}" start
# 7. DB Manager
run_init_script init_db_manager.sh start
# 8. Monitoring (Grafana, Prometheus)
# 7. Monitoring (Grafana, Prometheus)
run_init_script init_monitoring.sh initialize
# 9. Nginx Ingress (non-k3d only)
# 8. Nginx Ingress (non-k3d only)
if [[ "$local_mode" != "k3d" ]]; then
run_init_script init_nginx_ingress.sh initialize
fi
# 10. Port forwards (k3d only)
# 9. Port forwards (k3d only)
if [[ "$local_mode" == "k3d" && -x "${ROOT_DIR}/etc/init_port_forwards.sh" ]]; then
run_init_script init_port_forwards.sh -c "${PROLE_CFG_DEFAULT}" start
fi

View File

@ -276,7 +276,6 @@ class ProleDeployment(ProleInstaller):
- init_cloudnative_pg.sh (CNPG operator)
- init_kong.sh (API gateway)
- init_postgrest.sh (PostgREST on port 3000)
- init_db_manager.sh (DB manager)
- init_monitoring.sh (Prometheus/Grafana stack)
- supabase/deploy.sh (Supabase stack, if enabled)
"""
@ -293,7 +292,6 @@ class ProleDeployment(ProleInstaller):
("init_common_services.sh", svc_args),
("init_kong.sh", ["start"]),
("init_postgrest.sh", ["start"]),
("init_db_manager.sh", ["start"]),
("init_monitoring.sh", ["initialize"]),
]

View File

@ -1,55 +1,58 @@
Network Discovery Summary:
Primary Router: 10.0.0.1 (eero_5d:50:f2)
DNS Servers: 10.0.0.4, 10.0.0.5, 100.100.100.100
DNS Servers: 10.0.0.4, 100.100.100.100, 10.0.0.5
Detected Devices:
- 10.0.0.35 [7e:19:9a:fe:9f:ed] (7e:19:9a:fe:9f:ed): Ports [22, 445, 5900, 11434, 88], Services: ['SSH', 'VNC', 'SMB/CIFS (Possible Windows/AD)', 'Ollama', 'Active Directory Related']
- 10.0.0.4 [b8:27:eb:b3:6f:2b] (RaspberryPiF_b3:6f:2b): Ports [22, 53, 443, 2049], Services: ['DNS', 'SSH', 'NFS', 'Web Server']
- 10.0.0.189 [00:17:88:a3:2f:cc] (PhilipsLight_a3:2f:cc): Ports [80, 443], Services: ['Web Server']
- 10.0.0.26 [c8:db:26:08:52:f8] (Logitech_08:52:f8): Ports [], Services: []
- 10.0.0.5 [b8:27:eb:88:3a:41] (RaspberryPiF_88:3a:41): Ports [22, 2049, 5900], Services: ['SSH', 'NFS', 'VNC']
- 10.0.0.6 [2c:cf:67:45:8d:97] (RaspberryPi_45:8d:97): Ports [22, 80, 443], Services: ['SSH', 'Web Server']
- 10.0.0.22 [2c:cf:67:45:8d:97] (RaspberryPi_45:8d:97): Ports [22, 80, 443], Services: ['SSH', 'Web Server']
- 10.0.0.1 [9c:57:bc:5d:50:f2] (eero_5d:50:f2): Ports [53], Services: ['DNS']
- 10.0.0.203 [00:11:32:3b:2f:08] (Synology_3b:2f:08): Ports [22, 80, 443, 2049, 445], Services: ['SSH', 'NFS', 'Web Server', 'SMB/CIFS (Possible Windows/AD)']
- 10.0.0.3 [2c:cf:67:8c:2b:47] (RaspberryPi_8c:2b:47): Ports [22, 53, 80, 443, 2049, 445, 5900, 88, 389, 636], Services: ['DNS', 'SSH', 'NFS', 'VNC', 'Web Server', 'SMB/CIFS (Possible Windows/AD)', 'Active Directory Related']
- 10.0.0.5 [b8:27:eb:88:3a:41] (RaspberryPiF_88:3a:41): Ports [22, 2049, 5900], Services: ['SSH', 'NFS', 'VNC']
- 10.0.0.4 [b8:27:eb:b3:6f:2b] (RaspberryPiF_b3:6f:2b): Ports [22, 53, 443, 2049], Services: ['DNS', 'SSH', 'NFS', 'Web Server']
- 10.0.0.6 [2c:cf:67:45:8d:97] (RaspberryPi_45:8d:97): Ports [22, 80, 443], Services: ['SSH', 'Web Server']
- 10.0.0.32 [48:a6:b8:a4:8e:cc] (Sonos_a4:8e:cc): Ports [], Services: []
- 10.0.0.33 [ec:b5:fa:b0:76:e4] (PhilipsLight_b0:76:e4): Ports [80, 443], Services: ['Web Server']
- 10.0.0.1 [9c:57:bc:5d:50:f2] (eero_5d:50:f2): Ports [53], Services: ['DNS']
- 10.0.0.2 [dc:a4:ca:ea:1a:2f] (Apple_ea:1a:2f): Ports [], Services: []
- 10.0.0.7 [2c:cf:67:ca:93:ce] (RaspberryPi_ca:93:ce): Ports [22], Services: ['SSH']
- 10.0.0.2 [dc:a4:ca:ea:1a:2f] (Apple_ea:1a:2f): Ports [], Services: []
- 10.0.0.22 [2c:cf:67:45:8d:97] (RaspberryPi_45:8d:97): Ports [22, 80, 443], Services: ['SSH', 'Web Server']
- 10.0.0.26 [c8:db:26:08:52:f8] (Logitech_08:52:f8): Ports [], Services: []
- 10.0.0.204 [50:eb:f6:56:f3:26] (ASUSTekCOMPU_56:f3:26): Ports [22, 3389, 445, 5900, 11434], Services: ['SSH', 'VNC', 'SMB/CIFS (Possible Windows/AD)', 'RDP (Windows)', 'Ollama']
- 10.0.0.33 [ec:b5:fa:b0:76:e4] (PhilipsLight_b0:76:e4): Ports [80, 443], Services: ['Web Server']
- 10.0.0.34 [3a:b4:ce:79:b0:55] (3a:b4:ce:79:b0:55): Ports [], Services: []
- 10.0.0.37 [54:07:7d:22:c0:b2] (Netgear_22:c0:b2): Ports [80], Services: ['Web Server']
- 10.0.0.45 [f8:b4:6a:30:a4:1f] (HewlettPacka_30:a4:1f): Ports [80, 443], Services: ['Web Server']
- 10.0.0.46 [d4:f7:d5:40:ab:17] (SonyInteract_40:ab:17): Ports [], Services: []
- 10.0.0.48 [0c:fe:45:53:f4:3c] (SonyInteract_53:f4:3c): Ports [], Services: []
- 10.0.0.52 [2c:cf:67:ca:93:ce] (RaspberryPi_ca:93:ce): Ports [22], Services: ['SSH']
- 10.0.0.41 [b8:27:eb:57:10:d9] (RaspberryPiF_57:10:d9): Ports [22], Services: ['SSH']
- 10.0.0.205 [a8:20:66:28:12:e7] (Apple_28:12:e7): Ports [22, 53, 445, 5900, 88], Services: ['DNS', 'SSH', 'VNC', 'SMB/CIFS (Possible Windows/AD)', 'Active Directory Related']
- 10.0.0.73 [4c:a9:19:b3:12:f8] (TuyaSmart_b3:12:f8): Ports [], Services: []
- 10.0.0.58 [7e:46:74:2a:b0:b2] (7e:46:74:2a:b0:b2): Ports [], Services: []
- 10.0.0.60 [c6:d0:1a:97:9f:e9] (c6:d0:1a:97:9f:e9): Ports [], Services: []
- 10.0.0.35\ [7e:19:9a:fe:9f:ed] (7e:19:9a:fe:9f:ed): Ports [], Services: []
- 10.0.0.95 [b8:27:eb:88:3a:41] (RaspberryPiF_88:3a:41): Ports [22, 2049, 5900], Services: ['SSH', 'NFS', 'VNC']
- 10.0.0.206 [00:a0:de:a2:0b:ef] (Yamaha_a2:0b:ef): Ports [80], Services: ['Web Server']
- 10.0.0.46 [d4:f7:d5:40:ab:17] (SonyInteract_40:ab:17): Ports [], Services: []
- 10.0.0.41 [b8:27:eb:57:10:d9] (RaspberryPiF_57:10:d9): Ports [22], Services: ['SSH']
- 10.0.0.107 [b8:06:0d:b7:7c:56] (TuyaSmart_b7:7c:56): Ports [], Services: []
- 10.0.0.100 [24:fc:e5:51:cf:74] (SamsungElect_51:cf:74): Ports [], Services: []
- 10.0.0.99 [20:28:bc:f1:dc:ba] (Visionscape_f1:dc:ba): Ports [], Services: []
- 10.0.0.189 [00:17:88:a3:2f:cc] (PhilipsLight_a3:2f:cc): Ports [80, 443], Services: ['Web Server']
- 10.0.0.73 [4c:a9:19:b3:12:f8] (TuyaSmart_b3:12:f8): Ports [], Services: []
- 10.0.0.206 [00:a0:de:a2:0b:ef] (Yamaha_a2:0b:ef): Ports [80], Services: ['Web Server']
- 10.0.0.94 [f0:20:ff:bf:9a:19] (Intel_bf:9a:19): Ports [], Services: []
Note: Potential host for NVIDIA cards (PC Hardware vendor)
- 10.0.0.95 [b8:27:eb:88:3a:41] (RaspberryPiF_88:3a:41): Ports [22, 2049, 5900], Services: ['SSH', 'NFS', 'VNC']
- 10.0.0.106 [48:a6:b8:a7:50:60] (Sonos_a7:50:60): Ports [], Services: []
- 10.0.0.99 [20:28:bc:f1:dc:ba] (Visionscape_f1:dc:ba): Ports [], Services: []
- 10.0.0.111 [ca:0b:6c:5d:0a:e5] (ca:0b:6c:5d:0a:e5): Ports [], Services: []
- 10.0.0.117 [40:f5:20:1e:5e:91] (Espressif_1e:5e:91): Ports [80], Services: ['Web Server']
- 10.0.0.113 [60:81:10:92:02:4e] (Apple_92:02:4e): Ports [], Services: []
- 10.0.0.123 [3c:ef:8c:96:e3:3c] (ZhejiangDahu_96:e3:3c): Ports [80], Services: ['Web Server']
- 10.0.0.124 [9c:8e:cd:02:73:c4] (AmcrestTechn_02:73:c4): Ports [80], Services: ['Web Server']
- 10.0.0.112 [f8:33:31:40:58:c0] (TexasInstrum_40:58:c0): Ports [80, 443], Services: ['Web Server']
- 10.0.0.117 [40:f5:20:1e:5e:91] (Espressif_1e:5e:91): Ports [80], Services: ['Web Server']
- 10.0.0.127 [58:55:ca:4a:60:6e] (Apple_4a:60:6e): Ports [22], Services: ['SSH']
- 10.0.0.130 [f8:bb:bf:a3:84:eb] (eero_a3:84:eb): Ports [53], Services: ['DNS']
- 10.0.0.125 [a0:60:32:04:73:ba] (AmcrestTechn_04:73:ba): Ports [80], Services: ['Web Server']
- 10.0.0.130 [f8:bb:bf:a3:84:eb] (eero_a3:84:eb): Ports [53], Services: ['DNS']
- 10.0.0.143 [60:5f:8d:7a:aa:32] (eero_7a:aa:32): Ports [53], Services: ['DNS']
- 10.0.0.128 [20:c9:d0:94:30:dd] (Apple_94:30:dd): Ports [22, 53, 5900], Services: ['DNS', 'SSH', 'VNC']
- 10.0.0.188 [00:16:6c:c5:1f:54] (SamsungElect_c5:1f:54): Ports [80, 443], Services: ['Web Server']
- 10.0.0.170 [b0:ee:7b:ca:73:99] (Roku_ca:73:99): Ports [], Services: []
- 10.0.0.145 [7c:a6:b0:04:db:1c] (7c:a6:b0:04:db:1c): Ports [], Services: []
- 10.0.0.175 [28:80:88:e4:10:1a] (Netgear_e4:10:1a): Ports [80], Services: ['Web Server']
- 10.0.0.155 [62:17:97:b3:06:a1] (62:17:97:b3:06:a1): Ports [], Services: []
- 10.0.0.160 [96:c3:fd:58:02:6c] (96:c3:fd:58:02:6c): Ports [], Services: []
- 10.0.0.170 [b0:ee:7b:ca:73:99] (Roku_ca:73:99): Ports [], Services: []
- 10.0.0.188 [00:16:6c:c5:1f:54] (SamsungElect_c5:1f:54): Ports [80, 443], Services: ['Web Server']
- 10.0.0.196 [60:5f:8d:88:08:f2] (eero_88:08:f2): Ports [53], Services: ['DNS']
- 10.0.0.208 [a0:ad:9f:30:84:5f] (ASUSTekCOMPU_30:84:5f): Ports [], Services: []
- 10.0.0.1\ [9c:57:bc:5d:50:f2] (eero_5d:50:f2): Ports [], Services: []
- 10.0.0.58 [7e:46:74:2a:b0:b2] (7e:46:74:2a:b0:b2): Ports [], Services: []
- 10.0.0.199 [7c:a6:b0:01:a7:33] (7c:a6:b0:01:a7:33): Ports [], Services: []
- 10.0.0.60 [c6:d0:1a:97:9f:e9] (c6:d0:1a:97:9f:e9): Ports [], Services: []
Ollama Instances found at: 10.0.0.35, 10.0.0.204
Ansible Inventory Summary:

View File

@ -116,7 +116,6 @@ _STATUS_SCRIPTS: list[str] = [
"init_openbao.sh",
"init_kong.sh",
"init_postgrest.sh",
"init_db_manager.sh",
"init_monitoring.sh",
"init_cnpg_backup.sh",
"init_port_forwards.sh",

View File

@ -0,0 +1,132 @@
from __future__ import annotations
import knoe.ui.screens.cluster_nodes as cluster_nodes
from knoe.ui.screens.cluster_nodes import ClusterNodesScreenMixin
def test_render_cluster_nodes_page_topology_messages_are_scrollable(monkeypatch):
class _DummyWidget:
def __init__(self, *_args, **_kwargs):
pass
def destroy(self):
return None
def place_forget(self):
return None
def configure(self, **_kwargs):
return None
class _DummyFrame(_DummyWidget):
def pack(self, *_args, **_kwargs):
return None
class _DummyScrollbar(_DummyWidget):
instances: list["_DummyScrollbar"] = []
def __init__(self, *_args, **kwargs):
super().__init__()
self.command = kwargs.get("command")
_DummyScrollbar.instances.append(self)
def pack(self, *_args, **_kwargs):
return None
def set(self, *_args, **_kwargs):
return None
def configure(self, **kwargs):
if "command" in kwargs:
self.command = kwargs["command"]
class _DummyText(_DummyWidget):
instances: list["_DummyText"] = []
def __init__(self, *_args, **kwargs):
super().__init__()
self.yscrollcommand = kwargs.get("yscrollcommand")
self.contents = ""
self.state = "normal"
_DummyText.instances.append(self)
def pack(self, *_args, **_kwargs):
return None
def insert(self, _idx: str, text: str):
self.contents += text
def see(self, *_args, **_kwargs):
return None
def configure(self, **kwargs):
if "state" in kwargs:
self.state = kwargs["state"]
def yview(self, *_args, **_kwargs):
return None
monkeypatch.setattr(cluster_nodes.tk, "Frame", _DummyFrame)
monkeypatch.setattr(cluster_nodes.tk, "Scrollbar", _DummyScrollbar)
monkeypatch.setattr(cluster_nodes.tk, "Text", _DummyText)
monkeypatch.setattr(cluster_nodes.tk, "Button", _DummyWidget)
class _DummyCanvas:
def __init__(self):
self._next_id = 1
def create_text(self, *_args, **_kwargs):
item = self._next_id
self._next_id += 1
return item
def create_window(self, *_args, **_kwargs):
item = self._next_id
self._next_id += 1
return item
class _DummyApp(ClusterNodesScreenMixin):
def __init__(self):
self.bg_canvas = _DummyCanvas()
self._canvas_items: list[int] = []
self._overlay_widgets: list[object] = []
self._cluster_nodes_topology_state = {
"phase": "complete",
"messages": [f"line-{i}" for i in range(1, 7)],
"topology": None,
"topology_path": "",
"selected": set(),
"busy": True,
"expanded": None,
}
def _clear_canvas_page(self):
return None
def _should_show_cluster_nodes_screen(self):
return True
def _render_title(self, *_args, **_kwargs):
return None
def _render_paragraph(self, *_args, **_kwargs):
return None
def _cluster_nodes_start_discovery(self, force: bool = False):
return None
def _cluster_nodes_render_summary_cards(self, _topology, _x, y):
return y
def _cluster_nodes_render_table(self, _topology, _x, y):
return y
app = _DummyApp()
app._render_cluster_nodes_page()
assert _DummyScrollbar.instances
assert _DummyText.instances
assert _DummyText.instances[0].state == "disabled"
assert "line-1" in _DummyText.instances[0].contents
assert "line-6" in _DummyText.instances[0].contents
assert _DummyScrollbar.instances[0].command is not None

View File

@ -312,15 +312,17 @@ class TestRunPostApplyScripts:
dep.prole_cfg_data["Deployment"] = {"KUBECONFIG_PATH": ""}
dep._run_script = MagicMock(return_value=0)
dep._run_supabase_deploy = MagicMock(return_value=True)
assert dep._run_post_apply_scripts() is True
assert dep._run_script.call_count == 6
with patch("prole.deployment.cnpg_initialize", return_value=None):
assert dep._run_post_apply_scripts() is True
assert dep._run_script.call_count == 4
def test_script_failure(self, tmp_path):
dep, ctrl = _make_deployment(tmp_path)
dep.prole_cfg_data["Deployment"] = {"KUBECONFIG_PATH": ""}
dep._run_script = MagicMock(return_value=1)
dep._run_supabase_deploy = MagicMock(return_value=True)
assert dep._run_post_apply_scripts() is False
with patch("prole.deployment.cnpg_initialize", return_value=None):
assert dep._run_post_apply_scripts() is False
def test_kerberos_flag(self, tmp_path):
dep, ctrl = _make_deployment(
@ -332,7 +334,8 @@ class TestRunPostApplyScripts:
dep.prole_cfg_data["Deployment"] = {"KUBECONFIG_PATH": ""}
dep._run_script = MagicMock(return_value=0)
dep._run_supabase_deploy = MagicMock(return_value=True)
dep._run_post_apply_scripts()
with patch("prole.deployment.cnpg_initialize", return_value=None):
dep._run_post_apply_scripts()
first_call = dep._run_script.call_args_list[0]
assert "init_common_services" in str(first_call)
assert "-k" in str(first_call)

View File

@ -139,9 +139,304 @@ def test_tk_init_scripts_runner_invokes_init_certmgr(tmp_path, monkeypatch):
assert "init_common_services.sh" not in called_scripts
# CNPG is now Python-owned; shell script must never be dispatched
assert "init_cloudnative_pg.sh" not in called_scripts
assert "init_db_manager.sh" not in called_scripts
assert screen.prole_cfg_data["Initialization Scripts"]["STATUS"] == "Completed"
def test_tk_init_scripts_common_services_prefers_service_namespace(
tmp_path, monkeypatch
):
import knoe.ui.screens.services as services
from knoe.ui.screens.services import ServicesScreenMixin
class _Var:
def __init__(self, value=""):
self._value = value
def get(self):
return self._value
class _Console:
def __init__(self):
self.buf: list[str] = []
def clear(self):
self.buf.clear()
def write(self, content: str):
self.buf.append(content)
class _Widget:
def winfo_exists(self):
return False
def configure(self, **_kwargs):
return None
class _Canvas:
def winfo_exists(self):
return False
def itemconfig(self, *_args, **_kwargs):
return None
class _ImmediateThread:
def __init__(self, target, daemon=True):
self._target = target
def start(self):
self._target()
monkeypatch.setattr(services.threading, "Thread", _ImmediateThread)
monkeypatch.setattr(services, "cnpg_initialize", lambda **_kw: None)
monkeypatch.setattr(services.registry_ops, "update", lambda **_kw: None)
monkeypatch.setattr(services.garage_store_ops, "update", lambda **_kw: None)
monkeypatch.setattr(services.opentofu_ops, "update", lambda **_kw: None)
monkeypatch.setattr(services.monitoring_ops, "initialize", lambda **_kw: None)
openbao_calls: list[dict] = []
def _openbao_update(**kwargs):
openbao_calls.append(kwargs)
monkeypatch.setattr(services.openbao_ops, "update", _openbao_update)
class _DummyScreen(ServicesScreenMixin):
KUBECTL_STATUS_TAB = "__kubectl_status__"
def __init__(self):
self._action_flags = {}
self.controller = MagicMock()
self.controller.run_script.return_value = 0
self._init_scripts_button = _Widget()
self.bg_canvas = _Canvas()
self._init_scripts_status_label = 0
self._script_tab_index = {}
self.script_consoles = {
"init_common_services.sh": _Console(),
"common_services_log": _Console(),
"init_certmgr.sh": _Console(),
"init_cloudnative_pg.sh": _Console(),
"init_cnpg_backup.sh": _Console(),
"init_kong.sh": _Console(),
"init_monitoring.sh": _Console(),
"init_nginx_ingress.sh": _Console(),
"init_port_forwards.sh": _Console(),
self.KUBECTL_STATUS_TAB: _Console(),
}
self.db_username = _Var("prole")
self.db_password = _Var("pw")
self.db_namespace = _Var("knoe-db")
self.kerberos_enabled = _Var(False)
self.kerberos_realm = _Var("")
self.kerberos_kdc = _Var("")
self.kerberos_user = _Var("")
self.kerberos_password = _Var("")
self.cluster_env = _Var("prole-service-cluster")
self.prole_cfg_data = {"Initialization Scripts": {"STATUS": ""}}
self._scripts_success = False
def safe_after(self, fn, delay: int = 0):
return fn()
def _deployment_mode(self):
return "k3s"
def _resolve_prole_logs_dir(self):
return tmp_path
def _get_service_namespace(self):
return "knoe-system"
def _sync_port_forward_mappings(self):
return None
def _update_legacy_port_mapping(self):
return None
def _record_install_log(self, _path):
return None
def _process_script_output_line(self, _line: str):
return None
def _run_cmd_capture(self, _args):
return 0, ""
def _save_prole_cfg(self):
return None
def update_footer(self):
return None
def check_services_status_async(self):
return None
screen = _DummyScreen()
screen.run_init_scripts()
assert openbao_calls
assert openbao_calls[0]["namespace"] == "knoe-system"
def test_tk_init_scripts_uses_fallback_database_namespace_for_secret_checks(
tmp_path, monkeypatch
):
import knoe.ui.screens.services as services
from knoe.ui.screens.services import ServicesScreenMixin
class _Var:
def __init__(self, value=""):
self._value = value
def get(self):
return self._value
class _Console:
def __init__(self):
self.buf: list[str] = []
def clear(self):
self.buf.clear()
def write(self, content: str):
self.buf.append(content)
class _Widget:
def winfo_exists(self):
return False
def configure(self, **_kwargs):
return None
class _Canvas:
def winfo_exists(self):
return False
def itemconfig(self, *_args, **_kwargs):
return None
class _ImmediateThread:
def __init__(self, target, daemon=True):
self._target = target
def start(self):
self._target()
monkeypatch.setattr(services.threading, "Thread", _ImmediateThread)
monkeypatch.setattr(services, "cnpg_initialize", lambda **_kw: None)
monkeypatch.setattr(services.registry_ops, "update", lambda **_kw: None)
monkeypatch.setattr(services.openbao_ops, "update", lambda **_kw: None)
monkeypatch.setattr(services.garage_store_ops, "update", lambda **_kw: None)
monkeypatch.setattr(services.opentofu_ops, "update", lambda **_kw: None)
monkeypatch.setattr(services.monitoring_ops, "initialize", lambda **_kw: None)
secret_check_calls: list[list[str]] = []
class _DummyScreen(ServicesScreenMixin):
KUBECTL_STATUS_TAB = "__kubectl_status__"
def __init__(self):
self._action_flags = {}
self.controller = MagicMock()
self.controller.run_script.return_value = 0
self._init_scripts_button = _Widget()
self.bg_canvas = _Canvas()
self._init_scripts_status_label = 0
self._script_tab_index = {}
self.script_consoles = {
"init_common_services.sh": _Console(),
"common_services_log": _Console(),
"init_certmgr.sh": _Console(),
"init_cloudnative_pg.sh": _Console(),
"init_cnpg_backup.sh": _Console(),
"init_kong.sh": _Console(),
"init_monitoring.sh": _Console(),
"init_nginx_ingress.sh": _Console(),
"init_port_forwards.sh": _Console(),
self.KUBECTL_STATUS_TAB: _Console(),
}
self.db_username = _Var("prole")
self.db_password = _Var("pw")
# Reproducer: UI field is blank
self.db_namespace = _Var("")
self.kerberos_enabled = _Var(False)
self.kerberos_realm = _Var("")
self.kerberos_kdc = _Var("")
self.kerberos_user = _Var("")
self.kerberos_password = _Var("")
self.cluster_env = _Var("prole-service-cluster")
self.prole_cfg_data = {
"Initialization Scripts": {"STATUS": ""},
"Global": {"DATABASE_NAMESPACE": "knoe-db"},
}
self._scripts_success = False
def safe_after(self, fn, delay: int = 0):
return fn()
def _deployment_mode(self):
return "k3s"
def _resolve_prole_logs_dir(self):
return tmp_path
def _get_service_namespace(self):
return "knoe-system"
def _sync_port_forward_mappings(self):
return None
def _update_legacy_port_mapping(self):
return None
def _record_install_log(self, _path):
return None
def _process_script_output_line(self, _line: str):
return None
def _run_cmd_capture(self, args):
secret_check_calls.append(args)
if args[:4] == ["kubectl", "get", "secret", "knoe-db-user"]:
return (0, "") if args[-1] == "knoe-db" else (1, "")
if args[:4] == ["kubectl", "get", "secret", "knoe-db-superuser"]:
return (0, "") if args[-1] == "knoe-db" else (1, "")
if args[:4] == ["kubectl", "get", "secret", "cnpg-admin-key"]:
return (0, "") if args[-1] == "knoe-db" else (1, "")
return 0, ""
def _save_prole_cfg(self):
return None
def update_footer(self):
return None
def check_services_status_async(self):
return None
screen = _DummyScreen()
screen.run_init_scripts()
assert screen.prole_cfg_data["Initialization Scripts"]["STATUS"] == "Completed"
checked_ns = {
args[-1]
for args in secret_check_calls
if args[:3] == ["kubectl", "get", "secret"] and "-n" in args
}
assert "knoe-db" in checked_ns
def test_init_monitoring_grafana_password_capture_does_not_wipe_monitoring_data_dir(
tmp_path, monkeypatch
):

View File

@ -317,3 +317,58 @@ def test_discover_cluster_topology_includes_storage_items(tmp_path, monkeypatch)
assert len(node.storage_items) == 1
assert node.storage_items[0].label == "Synology d004"
assert node.storage_items[0].io_class == "io_slow_durable"
def test_collector_fs_stats_permission_denied_does_not_abort_collection() -> None:
script = topology_mod._collector_python_script()
namespace: dict[str, object] = {}
exec(script, namespace)
class _PathWithDeniedExists:
def __init__(self, value: str) -> None:
self.value = value
def __truediv__(self, other: str):
base = self.value.rstrip("/")
child = str(other).lstrip("/")
return _PathWithDeniedExists(f"{base}/{child}" if base else f"/{child}")
def exists(self) -> bool:
raise PermissionError("denied")
def __str__(self) -> str:
return self.value
class _Stat:
f_frsize = 4096
f_blocks = 1000
f_bavail = 200
f_bfree = 300
def fake_statvfs(path: str):
if "run/user/1000/doc" in path:
raise PermissionError("denied")
return _Stat()
namespace["Path"] = _PathWithDeniedExists
namespace["_mount_rows"] = lambda: [
("/dev/sda1", "/run/user/1000/doc", "ext4"),
("/dev/sdb1", "/data", "ext4"),
]
namespace["_read_rotational"] = lambda _block_name: None
namespace["_read_transport"] = lambda *_args: "unknown"
namespace["_infer_storage_class"] = lambda *_args: "unknown"
namespace["os"].statvfs = fake_statvfs
filesystems = namespace["_collect_filesystems"]()
assert len(filesystems) == 2
first = filesystems[0]
second = filesystems[1]
assert first["mountpoint"] == "/run/user/1000/doc"
assert first["sizeBytes"] is None
assert first["usedBytes"] is None
assert first["availableBytes"] is None
assert second["mountpoint"] == "/data"
assert second["sizeBytes"] == 4096 * 1000
assert second["availableBytes"] == 4096 * 200

View File

@ -139,3 +139,18 @@ def test_service_namespace_infers_registry_namespace(monkeypatch):
)
assert Dummy()._get_service_namespace() == "knoe-system"
def test_service_namespace_rejects_context_like_value_and_falls_back():
"""Regression: kube context names must not be treated as Kubernetes namespaces."""
class Dummy(ClusterScreenMixin):
def __init__(self):
self.service_namespace = _Var("knoe.dev.prole.org")
self.cluster_env = _Var("service")
self.prole_cfg_data = {"Global": {}}
def _infer_common_services_namespace(self) -> str:
return ""
assert Dummy()._get_service_namespace() == "knoe-system"

View File

@ -180,6 +180,8 @@ def test_dockerfile_generation_percona_18(mock_installer, tmp_path):
# Percona build flow should not install non-existent cron package variants
assert "percona-postgresql-18-cron" not in content
assert "percona-postgresql-18-pg_cron" not in content
# pg_tde is handled by dedicated Percona package naming and must not use generic mapping
assert "percona-postgresql-18-pg_tde" not in content
# SQL creation steps
assert "CREATE EXTENSION IF NOT EXISTS pgcrypto;" in content