mirror of
https://github.com/dredx/prole.git
synced 2026-09-23 10:13:58 +00:00
Summary: Removed the prole-db-manager microservice and simplified deployment to use prole-authority as the internal management and authorization point. Fixed two blocking bugs that prevented silent install from completing on knoe-dev-cluster. Removed: prole-db-manager - Deleted db-manager-deployment.yaml and db-manager-service.yaml from opentofu manifests - Deleted src/db-manager/ (Dockerfile, server.js, package.json, tests) - Removed prole-db-manager port-forward mapping from installer/core/env.py - Removed init_db_manager.sh from Initialization Scripts (milestones.py, actions.py) - Removed init_certmgr.sh and init_db_manager.sh tabs from services screen (services.py) - Removed live k8s Deployment/Service from knoe-dev-cluster Fixed: PostgreSQL version downgrade error (pg17 -> pg18) - Created conf/postgresql/.version with value 18 - Updated k8s/prole/prole-db.yaml and prole-db-recovery.yaml.tpl imageName to prole-db:18-089 - Fixed _init_database_options_state() to restore saved version_type from prole.cfg so db_version_type defaults to v18 (pg18) instead of silently reverting to pg17 - Added database_options.* keys to _collect_input_snapshot() in cfg.py so distribution, version_type, and all extension toggles persist to prole.cfg Fixed: Cluster name inconsistency - Removed stale prole-dev-cluster references; all scripts now use knoe-dev-cluster - Added knoe-dev-cluster to mode-detection case in etc/prole_cfg.sh Config: conf/prole.cfg - Set kerberos_config.enabled = False, KERBEROS_AUTO_ENABLED = False - Added database_options.distribution = percona, version_type = v18 - Added all 13 extension flags set to True (postgis, pgvector, pgcrypto, pgaudit, pg_repack, pg_stat_statements, pg_buffercache, pg_freespacemap, pgrowlocks, postgres_fdw, dblink, pg_stat_monitor, pgbadger) Verification: ./install.py -s -l -v -c conf/prole.cfg completed successfully. CNPG deployed prole-db:18-089 to knoe-dev-cluster; all milestones passed. Co-authored-by: Junie <junie@jetbrains.com>
896 lines
32 KiB
Python
896 lines
32 KiB
Python
"""prole.cfg persistence, input snapshot collection and port-forward management."""
|
|
|
|
import configparser
|
|
import os
|
|
import platform
|
|
import socket
|
|
import string
|
|
import subprocess
|
|
from pathlib import Path
|
|
import tkinter as tk
|
|
from tkinter import ttk, messagebox, filedialog
|
|
from installer.core.env import (
|
|
DEFAULT_ACTION_FLAGS,
|
|
DEFAULT_OLLAMA_PORT,
|
|
PROJECT_ROOT,
|
|
_bool_str,
|
|
_build_required_port_forwards,
|
|
_cluster_env_radio_value,
|
|
_default_opentofu_pipeline_url,
|
|
_deployment_mode_from_env,
|
|
_deployment_target_label,
|
|
_detect_ansible_topology,
|
|
_format_ansible_topology_summary,
|
|
_normalize_k3s_token,
|
|
_pf_extract_id,
|
|
_pf_upsert_mapping,
|
|
_read_k3s_cfg,
|
|
_render_prole_cfg,
|
|
)
|
|
from installer.config import _collect_cfg_vars, _encrypt_cfg_secret, _expand_cfg_value
|
|
|
|
|
|
class ConfigMixin:
|
|
"""prole.cfg persistence, input snapshot collection and port-forward management."""
|
|
|
|
def _save_prole_cfg(self):
|
|
"""Generates prole.cfg; master configuration file containing all values used in install process."""
|
|
try:
|
|
# Check port availability before saving
|
|
# try:
|
|
# port_val = int(self.db_host_port.get().strip())
|
|
# if not self._is_port_available(port_val):
|
|
# self._show_port_error(port_val)
|
|
# return
|
|
# except ValueError:
|
|
# messagebox.showerror("Invalid Port", "Please enter a valid numeric port.")
|
|
# return
|
|
|
|
# Determine path: conf/prole.cfg
|
|
# If PROLE_CONF is set, use it. Otherwise fallback to PROJECT_ROOT/conf
|
|
if self._cfg_path_override is not None:
|
|
cfg_path = self._cfg_path_override
|
|
if cfg_path.is_dir():
|
|
conf_dir = cfg_path
|
|
cfg_path = conf_dir / "prole.cfg"
|
|
else:
|
|
conf_dir = cfg_path.parent
|
|
else:
|
|
prole_conf = os.environ.get("PROLE_CONF")
|
|
if prole_conf:
|
|
conf_dir = Path(prole_conf).expanduser()
|
|
else:
|
|
conf_dir = PROJECT_ROOT / "conf"
|
|
os.environ.setdefault("PROLE_CONF", str(conf_dir))
|
|
cfg_path = conf_dir / "prole.cfg"
|
|
|
|
conf_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
# Identify global candidates
|
|
mode = self._deployment_mode()
|
|
target_label = _deployment_target_label(self.cluster_env.get())
|
|
globals_to_save = {
|
|
"PROLE_HOME": os.environ.get("PROLE_HOME", ""),
|
|
"PROLE_DB_USER": self.db_username.get(),
|
|
"DB_PASSWORD": self._secret_cfg_value(
|
|
"Global", "DB_PASSWORD", self.db_password.get(), "db", "password"
|
|
),
|
|
"CLUSTER_ENV": self.cluster_env.get(),
|
|
"DEPLOYMENT_MODE": mode,
|
|
"DEPLOYMENT_TARGET": target_label,
|
|
"NAMESPACE": (self.db_namespace.get() or "").strip(),
|
|
"DB_HOST_PORT": (self.db_host_port.get() or "5432").strip(),
|
|
"PROLE_OPENTOFU_URL": _default_opentofu_pipeline_url(),
|
|
}
|
|
# Merge any other values already present in existing Global without overriding explicit UI values
|
|
existing_global = dict(self.prole_cfg_data.get("Global", {}))
|
|
for k, v in existing_global.items():
|
|
if k not in globals_to_save or not str(globals_to_save.get(k, "")).strip():
|
|
globals_to_save[k] = v
|
|
# Re-assert dynamic/derived globals to ensure they are not overwritten by existing values
|
|
globals_to_save["DB_PASSWORD"] = self._secret_cfg_value(
|
|
"Global", "DB_PASSWORD", self.db_password.get(), "db", "password"
|
|
)
|
|
globals_to_save["DEPLOYMENT_MODE"] = mode
|
|
globals_to_save["DEPLOYMENT_TARGET"] = target_label
|
|
globals_to_save["PROLE_K3S_SERVER"] = (
|
|
self.k3s_server_url.get() or ""
|
|
).strip()
|
|
globals_to_save["PROLE_K3S_TOKEN"] = _encrypt_cfg_secret(
|
|
self.k3s_token.get() or ""
|
|
)
|
|
globals_to_save["SERVICE_NAMESPACE"] = self._get_service_namespace()
|
|
|
|
self._sync_port_forward_mappings()
|
|
|
|
# Inputs section (replayable UI inputs)
|
|
try:
|
|
inputs = self._collect_input_snapshot()
|
|
except Exception:
|
|
inputs = {}
|
|
|
|
sections = {
|
|
k: self.prole_cfg_data.get(k, {})
|
|
for k in [
|
|
"Welcome",
|
|
"Dependencies",
|
|
"Network",
|
|
"System Environment",
|
|
"Monitoring",
|
|
"Kerberos Authentication",
|
|
"Ollama",
|
|
"Optional Features",
|
|
"Database Creation",
|
|
"Initialize Cluster",
|
|
"Docker Build",
|
|
"Initialization Scripts",
|
|
"Deployment",
|
|
"Install",
|
|
]
|
|
}
|
|
deployment_section = dict(sections.get("Deployment", {}))
|
|
if mode:
|
|
deployment_section.setdefault("MODE", mode)
|
|
if target_label:
|
|
deployment_section.setdefault("TARGET", target_label)
|
|
sections["Deployment"] = deployment_section
|
|
|
|
sections["Dev Cluster (k3d)"] = {
|
|
**self.prole_cfg_data.get("Dev Cluster (k3d)", {}),
|
|
"MODE": "k3d",
|
|
"CLUSTER_ENV": "dev",
|
|
"DISPLAY_NAME": "knoe-dev-cluster",
|
|
"KUBECTL_CONTEXT": (self.selected_kubectx.get() or "").strip(),
|
|
}
|
|
sections["Service Cluster (k3s)"] = {
|
|
**self.prole_cfg_data.get("Service Cluster (k3s)", {}),
|
|
"MODE": "k3s",
|
|
"CLUSTER_ENV": "prole-service-cluster",
|
|
"DISPLAY_NAME": "prole-service-cluster",
|
|
"K3S_SERVER_URL": (self.k3s_server_url.get() or "").strip(),
|
|
"K3S_TOKEN": _encrypt_cfg_secret(self.k3s_token.get() or ""),
|
|
"PIPELINE_URL": _default_opentofu_pipeline_url(),
|
|
}
|
|
sections["Prod Cluster (k8s)"] = {
|
|
**self.prole_cfg_data.get("Prod Cluster (k8s)", {}),
|
|
"MODE": "k8s",
|
|
"CLUSTER_ENV": "prole-prod-cluster",
|
|
"DISPLAY_NAME": "prole-prod-cluster",
|
|
"ARTIFACTS_DIR": (self.prod_artifacts_path.get() or "").strip(),
|
|
"PIPELINE_URL": _default_opentofu_pipeline_url(),
|
|
}
|
|
sections = self._sanitize_sections_for_cfg(sections)
|
|
|
|
# Sync to Ansible Prole Vault
|
|
self._save_ansible_prole_vault(self.db_password.get())
|
|
|
|
cfg_text = _render_prole_cfg(inputs, globals_to_save, sections)
|
|
cfg_path.write_text(cfg_text)
|
|
print(f"[DEBUG] prole.cfg saved to {cfg_path}")
|
|
|
|
except Exception as e:
|
|
print(f"[ERROR] Failed to save prole.cfg: {e}")
|
|
|
|
def _collect_input_snapshot(self) -> dict:
|
|
"""Collect all possible user inputs for prole.cfg replay."""
|
|
inputs: dict[str, str] = {}
|
|
|
|
def _set(key: str, val):
|
|
inputs[key] = "" if val is None else str(val)
|
|
|
|
def _set_bool(key: str, val: bool):
|
|
inputs[key] = _bool_str(bool(val))
|
|
|
|
def _get_var(var, default=""):
|
|
try:
|
|
return var.get()
|
|
except Exception:
|
|
return default
|
|
|
|
def _action(key: str, default: bool):
|
|
return bool(self._action_flags.get(key, default))
|
|
|
|
# Dependencies
|
|
_set_bool("dependencies.verify_all", _get_var(self.verify_mode, False))
|
|
_set_bool(
|
|
"dependencies.auto_install_missing",
|
|
_action(
|
|
"dependencies.auto_install_missing",
|
|
DEFAULT_ACTION_FLAGS.get("dependencies.auto_install_missing", True),
|
|
),
|
|
)
|
|
for dep in self.dependencies:
|
|
dep_key = f"dependencies.{dep['id']}.install"
|
|
_set_bool(dep_key, _action(dep_key, True))
|
|
|
|
# Network scan
|
|
_set_bool(
|
|
"network_scan.run",
|
|
_action(
|
|
"network_scan.run", DEFAULT_ACTION_FLAGS.get("network_scan.run", True)
|
|
),
|
|
)
|
|
|
|
# Environment setup values
|
|
env_vals = {}
|
|
try:
|
|
env_vals.update(self._env_defaults())
|
|
except Exception:
|
|
pass
|
|
try:
|
|
env_vals.update(self._read_existing_env())
|
|
except Exception:
|
|
pass
|
|
for k in (
|
|
"PROLE_HOME",
|
|
"PROLE_CONF",
|
|
"PROLE_DATA",
|
|
"PROLE_LOGS",
|
|
"PROLE_SERVICE",
|
|
):
|
|
if os.environ.get(k):
|
|
env_vals[k] = os.environ.get(k)
|
|
if hasattr(self, "_env_entries"):
|
|
for k, ent in self._env_entries.items():
|
|
try:
|
|
v = ent.get().strip()
|
|
if v:
|
|
env_vals[k] = v
|
|
except Exception:
|
|
pass
|
|
for k in (
|
|
"PROLE_HOME",
|
|
"PROLE_CONF",
|
|
"PROLE_DATA",
|
|
"PROLE_LOGS",
|
|
"PROLE_SERVICE",
|
|
):
|
|
_set(f"env_setup.{k}", env_vals.get(k, ""))
|
|
ns_val = (
|
|
(str(_get_var(self.db_namespace, "")).strip())
|
|
if hasattr(self, "db_namespace")
|
|
else ""
|
|
)
|
|
if not ns_val:
|
|
ns_val = env_vals.get("NAMESPACE", "")
|
|
_set("env_setup.NAMESPACE", ns_val)
|
|
|
|
# Database creation
|
|
_set("init_password.db_namespace", ns_val)
|
|
_set("init_password.db_username", _get_var(self.db_username, ""))
|
|
db_pw = _get_var(self.db_password, "")
|
|
db_pw_confirm = _get_var(self.db_password_confirm, "") or db_pw
|
|
db_pw_cfg = self._secret_cfg_value(
|
|
"Inputs", "init_password.db_password", db_pw, "db", "password"
|
|
)
|
|
_set("init_password.db_password", db_pw_cfg)
|
|
_set("init_password.db_password_confirm", db_pw_cfg or db_pw_confirm)
|
|
_set("init_password.db_host_port", _get_var(self.db_host_port, "5432"))
|
|
_set_bool(
|
|
"init_password.generate_ssh_key",
|
|
_action(
|
|
"init_password.generate_ssh_key",
|
|
DEFAULT_ACTION_FLAGS.get("init_password.generate_ssh_key", True),
|
|
),
|
|
)
|
|
|
|
# Database options (distribution, version, extensions)
|
|
_set("database_options.distribution", _get_var(self.db_distribution, "percona"))
|
|
_set("database_options.version_type", _get_var(self.db_version_type, "v18"))
|
|
for _ext in getattr(self, "extensions_list", []):
|
|
_set_bool(
|
|
f"database_options.ext.{_ext['id']}",
|
|
_get_var(self.db_extensions.get(_ext["id"]), False),
|
|
)
|
|
|
|
# Build DB image
|
|
_set_bool(
|
|
"init_db_build.run_build",
|
|
_action(
|
|
"init_db_build.run_build",
|
|
DEFAULT_ACTION_FLAGS.get("init_db_build.run_build", True),
|
|
),
|
|
)
|
|
|
|
# Cluster init + optional features
|
|
_set("init_cluster.cluster_env", _get_var(self.cluster_env, "dev"))
|
|
_set(
|
|
"init_cluster.mode",
|
|
_deployment_mode_from_env(_get_var(self.cluster_env, "dev")),
|
|
)
|
|
_set(
|
|
"init_cluster.deployment_target",
|
|
_deployment_target_label(_get_var(self.cluster_env, "dev")),
|
|
)
|
|
_set("init_cluster.k3s_server_url", _get_var(self.k3s_server_url, ""))
|
|
_set(
|
|
"init_cluster.k3s_token", _encrypt_cfg_secret(_get_var(self.k3s_token, ""))
|
|
)
|
|
_set_bool(
|
|
"init_cluster.supabase_enabled", _get_var(self.supabase_enabled, False)
|
|
)
|
|
_set_bool("init_cluster.gitops_enabled", _get_var(self.gitops_enabled, False))
|
|
_set_bool(
|
|
"init_cluster.kerberos_enabled", _get_var(self.kerberos_enabled, False)
|
|
)
|
|
_set_bool(
|
|
"init_cluster.at_rest_encryption_enabled",
|
|
_get_var(self.at_rest_encryption_enabled, False),
|
|
)
|
|
_set_bool(
|
|
"init_cluster.start_cluster",
|
|
_action(
|
|
"init_cluster.start_cluster",
|
|
DEFAULT_ACTION_FLAGS.get("init_cluster.start_cluster", True),
|
|
),
|
|
)
|
|
|
|
# Kerberos config
|
|
_set_bool("kerberos_config.enabled", _get_var(self.kerberos_enabled, False))
|
|
_set("kerberos_config.realm", _get_var(self.kerberos_realm, ""))
|
|
_set("kerberos_config.kdc", _get_var(self.kerberos_kdc, ""))
|
|
_set("kerberos_config.user", _get_var(self.kerberos_user, ""))
|
|
krb_pw = _get_var(self.kerberos_password, "")
|
|
_set(
|
|
"kerberos_config.password",
|
|
self._secret_cfg_value(
|
|
"Inputs", "kerberos_config.password", krb_pw, "kerberos", "password"
|
|
),
|
|
)
|
|
_set_bool(
|
|
"kerberos_config.test_connection",
|
|
_action(
|
|
"kerberos_config.test_connection",
|
|
DEFAULT_ACTION_FLAGS.get("kerberos_config.test_connection", False),
|
|
),
|
|
)
|
|
|
|
# Ollama config
|
|
_set("ollama_config.server_host", _get_var(self.ollama_server_host, ""))
|
|
_set(
|
|
"ollama_config.server_port",
|
|
_get_var(self.ollama_server_port, DEFAULT_OLLAMA_PORT),
|
|
)
|
|
_set("ollama_config.model", _get_var(self.ollama_model, ""))
|
|
|
|
# Init scripts + deploy
|
|
_set_bool(
|
|
"init_scripts.run_scripts",
|
|
_action(
|
|
"init_scripts.run_scripts",
|
|
DEFAULT_ACTION_FLAGS.get("init_scripts.run_scripts", True),
|
|
),
|
|
)
|
|
_set_bool(
|
|
"init_cnpg_deploy.run_deploy",
|
|
_action(
|
|
"init_cnpg_deploy.run_deploy",
|
|
DEFAULT_ACTION_FLAGS.get("init_cnpg_deploy.run_deploy", True),
|
|
),
|
|
)
|
|
_set_bool(
|
|
"init_cnpg_deploy.force_rollout",
|
|
_action(
|
|
"init_cnpg_deploy.force_rollout",
|
|
DEFAULT_ACTION_FLAGS.get("init_cnpg_deploy.force_rollout", False),
|
|
),
|
|
)
|
|
|
|
# GitOps
|
|
_set("gitops.namespace", _get_var(self.gitops_namespace, "gitea"))
|
|
|
|
# Disk selection (installer packaging)
|
|
_set("disk_selection.disk_type", _get_var(self.selected_disk_type, "local"))
|
|
_set(
|
|
"disk_selection.removable_mount", _get_var(self.selected_removable_disk, "")
|
|
)
|
|
_set(
|
|
"disk_selection.local_path",
|
|
_get_var(self.selected_local_path, str(Path.home())),
|
|
)
|
|
|
|
# Build tools app
|
|
_set("build.deploy_env", getattr(self, "deploy_env_value", "Dev"))
|
|
_set_bool(
|
|
"build.run_build",
|
|
_action(
|
|
"build.run_build", DEFAULT_ACTION_FLAGS.get("build.run_build", False)
|
|
),
|
|
)
|
|
|
|
return inputs
|
|
|
|
def _sync_port_forward_mappings(self):
|
|
try:
|
|
mode = self._deployment_mode()
|
|
except Exception:
|
|
mode = ""
|
|
if not mode:
|
|
try:
|
|
mode = _deployment_mode_from_env(self.cluster_env.get())
|
|
except Exception:
|
|
mode = ""
|
|
if not mode:
|
|
mode = "k3d"
|
|
prefix = (
|
|
"PORT_FORWARD_K3S_MAPPING_"
|
|
if mode == "k3s"
|
|
else "PORT_FORWARD_K3D_MAPPING_"
|
|
)
|
|
|
|
pf_section = self.prole_cfg_data.get("Port Forwards", {})
|
|
if pf_section is None:
|
|
pf_section = {}
|
|
|
|
try:
|
|
service_ns = (self._get_service_namespace() or "").strip()
|
|
except Exception:
|
|
service_ns = ""
|
|
if not service_ns:
|
|
service_ns = "default"
|
|
|
|
argocd_ns = (
|
|
(self.prole_cfg_data.get("Global", {}) or {})
|
|
.get("ARGOCD_NAMESPACE", "")
|
|
.strip()
|
|
)
|
|
if not argocd_ns:
|
|
argocd_ns = (os.environ.get("ARGOCD_NAMESPACE") or "").strip()
|
|
if not argocd_ns:
|
|
argocd_ns = "argocd"
|
|
|
|
try:
|
|
db_ns = (self.db_namespace.get() or "").strip()
|
|
except Exception:
|
|
db_ns = ""
|
|
if not db_ns:
|
|
db_ns = (
|
|
(self.prole_cfg_data.get("Global", {}) or {})
|
|
.get("NAMESPACE", "")
|
|
.strip()
|
|
)
|
|
if not db_ns:
|
|
db_ns = (os.environ.get("NAMESPACE") or "").strip()
|
|
if not db_ns:
|
|
db_ns = "default"
|
|
|
|
try:
|
|
db_host_port = (self.db_host_port.get() or "").strip()
|
|
except Exception:
|
|
db_host_port = ""
|
|
if not db_host_port:
|
|
db_host_port = (
|
|
(self.prole_cfg_data.get("Global", {}) or {})
|
|
.get("DB_HOST_PORT", "")
|
|
.strip()
|
|
)
|
|
if not db_host_port:
|
|
db_host_port = "5432"
|
|
|
|
supabase_enabled = False
|
|
try:
|
|
supabase_enabled = bool(self.supabase_enabled.get())
|
|
except Exception:
|
|
supabase_enabled = False
|
|
|
|
supabase_ns = (os.environ.get("SUPABASE_NAMESPACE") or "").strip()
|
|
if not supabase_ns:
|
|
supabase_ns = (
|
|
(self.prole_cfg_data.get("Supabase", {}) or {})
|
|
.get("NAMESPACE", "")
|
|
.strip()
|
|
)
|
|
if not supabase_ns:
|
|
supabase_ns = "supabase"
|
|
|
|
gitops_enabled = False
|
|
try:
|
|
gitops_enabled = bool(self.gitops_enabled.get())
|
|
except Exception:
|
|
gitops_enabled = False
|
|
gitops_ns = (os.environ.get("GITEA_NAMESPACE") or "").strip()
|
|
if not gitops_ns:
|
|
gitops_ns = (
|
|
(self.prole_cfg_data.get("GitOps", {}) or {})
|
|
.get("GITOPS_NAMESPACE", "")
|
|
.strip()
|
|
)
|
|
if not gitops_ns:
|
|
gitops_ns = (self.gitops_namespace.get() or "").strip() or "gitea"
|
|
|
|
mappings = _build_required_port_forwards(
|
|
mode=mode,
|
|
service_ns=service_ns,
|
|
argocd_ns=argocd_ns,
|
|
db_ns=db_ns,
|
|
db_host_port=db_host_port,
|
|
supabase_enabled=supabase_enabled,
|
|
supabase_namespace=supabase_ns,
|
|
gitops_enabled=gitops_enabled,
|
|
gitops_namespace=gitops_ns,
|
|
)
|
|
for mapping in mappings:
|
|
_pf_upsert_mapping(pf_section, prefix, mapping)
|
|
|
|
self.prole_cfg_data["Port Forwards"] = pf_section
|
|
self._update_legacy_port_mapping()
|
|
|
|
def _update_legacy_port_mapping(self):
|
|
conf_dir = PROJECT_ROOT / "conf"
|
|
mapping_path = conf_dir / "port-mapping.cfg"
|
|
|
|
pf_section = self.prole_cfg_data.get("Port Forwards", {})
|
|
if pf_section is None:
|
|
pf_section = {}
|
|
|
|
try:
|
|
mode = self._deployment_mode()
|
|
except Exception:
|
|
mode = "k3d"
|
|
prefix = (
|
|
"PORT_FORWARD_K3S_MAPPING_"
|
|
if mode == "k3s"
|
|
else "PORT_FORWARD_K3D_MAPPING_"
|
|
)
|
|
|
|
# Resolve ${NAMESPACE} for port-mapping entries
|
|
try:
|
|
db_ns = self.db_namespace.get().strip()
|
|
except Exception:
|
|
db_ns = ""
|
|
if not db_ns:
|
|
db_ns = (
|
|
(self.prole_cfg_data.get("Global", {}) or {})
|
|
.get("NAMESPACE", "")
|
|
.strip()
|
|
)
|
|
if not db_ns:
|
|
db_ns = "default"
|
|
|
|
lines = [
|
|
"# Port mappings for Prole Tools (generated).",
|
|
"# Format: key: local=... remote=... ns=... svc=... address=...",
|
|
"",
|
|
]
|
|
|
|
# We want to maintain some order or just dump them
|
|
for k, v in sorted(pf_section.items()):
|
|
if k.startswith(prefix):
|
|
# Parse the mapping string: id=...;namespace=...;target=...;address=...;hostPort=...;servicePort=...;protocol=...;description=...
|
|
parts = {}
|
|
for p in str(v).split(";"):
|
|
if "=" in p:
|
|
key_val = p.split("=", 1)
|
|
if len(key_val) == 2:
|
|
parts[key_val[0].strip()] = key_val[1].strip()
|
|
|
|
if "id" in parts:
|
|
m_id = parts["id"]
|
|
local = parts.get("hostPort", "")
|
|
remote = parts.get("servicePort", "")
|
|
ns = parts.get("namespace", "")
|
|
target = parts.get("target", "")
|
|
addr = parts.get("address", "0.0.0.0")
|
|
|
|
# Resolve ${NAMESPACE} references
|
|
if ns.startswith("${") and "NAMESPACE" in ns:
|
|
ns = db_ns
|
|
# Sanitize: skip entries with non-string or mock values
|
|
vals = [m_id, local, remote, ns, target, addr]
|
|
if any(
|
|
"MagicMock" in str(x) or not isinstance(x, str) for x in vals
|
|
):
|
|
continue
|
|
if local.startswith("${"):
|
|
continue
|
|
|
|
svc = target
|
|
if svc.startswith("svc/"):
|
|
svc = svc[4:]
|
|
|
|
lines.append(
|
|
f"{m_id}: local={local} remote={remote} ns={ns} svc={svc} address={addr}"
|
|
)
|
|
|
|
try:
|
|
mapping_path.write_text("\n".join(lines) + "\n")
|
|
print(f"[DEBUG] port-mapping.cfg updated at {mapping_path}")
|
|
except Exception as e:
|
|
print(f"[ERROR] Failed to update port-mapping.cfg: {e}")
|
|
|
|
def _add_port_mapping(self, mapping_str):
|
|
mode = self._deployment_mode()
|
|
prefix = (
|
|
"PORT_FORWARD_K3S_MAPPING_"
|
|
if mode == "k3s"
|
|
else "PORT_FORWARD_K3D_MAPPING_"
|
|
)
|
|
|
|
pf_section = self.prole_cfg_data.get("Port Forwards", {})
|
|
if pf_section is None:
|
|
pf_section = {}
|
|
|
|
if not _pf_upsert_mapping(pf_section, prefix, mapping_str):
|
|
return
|
|
|
|
self.prole_cfg_data["Port Forwards"] = pf_section
|
|
self._save_prole_cfg()
|
|
|
|
def _process_script_output_line(self, line):
|
|
if "GRAFANA_ADMIN_PASSWORD=" in line:
|
|
pwd = line.split("GRAFANA_ADMIN_PASSWORD=")[1].strip()
|
|
if pwd:
|
|
mon = self.prole_cfg_data.get("Monitoring", {})
|
|
if mon is None:
|
|
mon = {}
|
|
mon["GRAFANA_ADMIN_PASSWORD"] = pwd
|
|
self.prole_cfg_data["Monitoring"] = mon
|
|
self._save_prole_cfg()
|
|
|
|
if "PORT_FORWARD_MAPPING:" in line:
|
|
mapping = line.split("PORT_FORWARD_MAPPING:")[1].strip()
|
|
if mapping:
|
|
if _pf_extract_id(mapping) == "registry":
|
|
return
|
|
self._add_port_mapping(mapping)
|
|
|
|
def _is_port_available(self, port: int) -> bool:
|
|
"""Check if a port is available on localhost."""
|
|
import socket
|
|
|
|
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
|
try:
|
|
s.bind(("127.0.0.1", port))
|
|
return True
|
|
except socket.error:
|
|
return False
|
|
|
|
def _pf_extract_field(self, mapping_str: str, field: str) -> str:
|
|
if not mapping_str or not field:
|
|
return ""
|
|
needle = f"{field}="
|
|
for part in str(mapping_str).split(";"):
|
|
part = part.strip()
|
|
if part.startswith(needle):
|
|
return part[len(needle) :].strip()
|
|
return ""
|
|
|
|
def _validate_port_forward_overlaps(self) -> bool:
|
|
try:
|
|
self._sync_port_forward_mappings()
|
|
except Exception:
|
|
pass
|
|
|
|
try:
|
|
mode = self._deployment_mode()
|
|
except Exception:
|
|
mode = ""
|
|
if not mode:
|
|
try:
|
|
mode = _deployment_mode_from_env(self.cluster_env.get())
|
|
except Exception:
|
|
mode = ""
|
|
if not mode:
|
|
mode = "k3d"
|
|
|
|
prefix = (
|
|
"PORT_FORWARD_K3S_MAPPING_"
|
|
if mode == "k3s"
|
|
else "PORT_FORWARD_K3D_MAPPING_"
|
|
)
|
|
pf_section = self.prole_cfg_data.get("Port Forwards", {}) or {}
|
|
|
|
port_index = {}
|
|
for key, mapping in pf_section.items():
|
|
if not key.startswith(prefix):
|
|
continue
|
|
host_port = self._pf_extract_field(mapping, "hostPort")
|
|
if not host_port:
|
|
continue
|
|
mapping_id = _pf_extract_id(mapping)
|
|
target = self._pf_extract_field(mapping, "target")
|
|
namespace = self._pf_extract_field(mapping, "namespace")
|
|
label = mapping_id or target or key
|
|
if namespace and target:
|
|
label = f"{label} ({namespace}/{target})"
|
|
port_index.setdefault(host_port, []).append(label)
|
|
|
|
collisions = {
|
|
port: labels for port, labels in port_index.items() if len(labels) > 1
|
|
}
|
|
if not collisions:
|
|
return True
|
|
|
|
lines = []
|
|
for port, labels in sorted(
|
|
collisions.items(),
|
|
key=lambda item: int(item[0]) if str(item[0]).isdigit() else item[0],
|
|
):
|
|
lines.append(f"{port}: {', '.join(sorted(labels))}")
|
|
|
|
try:
|
|
messagebox.showerror(
|
|
"Port Collision",
|
|
"Port forward mappings overlap:\n\n"
|
|
+ "\n".join(lines)
|
|
+ "\n\nPlease choose unique host ports before continuing.",
|
|
)
|
|
except Exception:
|
|
pass
|
|
return False
|
|
|
|
def _show_port_error(self, port: int):
|
|
"""Show error message when port is in use, with option to see lsof output."""
|
|
msg = f"Port {port} is already in use on the local host.\n\nPlease choose a different port or stop the service using it."
|
|
|
|
dialog = tk.Toplevel(self.root)
|
|
dialog.title("Port In Use")
|
|
dialog.geometry("450x200")
|
|
dialog.configure(bg="white")
|
|
dialog.transient(self.root)
|
|
dialog.grab_set()
|
|
|
|
tk.Label(
|
|
dialog,
|
|
text="Port Conflict",
|
|
font=("SF Pro Text", 14, "bold"),
|
|
bg="white",
|
|
fg="#ff3b30",
|
|
).pack(pady=(20, 10))
|
|
tk.Label(
|
|
dialog,
|
|
text=msg,
|
|
font=("SF Pro Text", 11),
|
|
bg="white",
|
|
fg="black",
|
|
wraplength=400,
|
|
).pack(pady=10)
|
|
|
|
btn_frame = tk.Frame(dialog, bg="white")
|
|
btn_frame.pack(pady=20)
|
|
|
|
def show_lsof():
|
|
log_file = PROJECT_ROOT / "logs" / f"port_{port}_lsof.log"
|
|
log_file.parent.mkdir(parents=True, exist_ok=True)
|
|
try:
|
|
res = subprocess.run(
|
|
["lsof", "-i", f":{port}"], capture_output=True, text=True
|
|
)
|
|
content = (
|
|
res.stdout
|
|
if res.stdout
|
|
else f"No lsof output for port {port}. (Maybe permission denied?)"
|
|
)
|
|
if res.stderr:
|
|
content += "\n\nError:\n" + res.stderr
|
|
log_file.write_text(content)
|
|
# Open the log file
|
|
if platform.system() == "Darwin":
|
|
subprocess.run(["open", str(log_file)])
|
|
elif platform.system() == "Windows":
|
|
os.startfile(str(log_file))
|
|
else:
|
|
subprocess.run(["xdg-open", str(log_file)])
|
|
except Exception as e:
|
|
messagebox.showerror("Error", f"Failed to run lsof: {e}")
|
|
|
|
tk.Button(
|
|
btn_frame,
|
|
text="Show Logs (lsof)",
|
|
command=show_lsof,
|
|
bg="#F5F5DC",
|
|
relief="flat",
|
|
padx=10,
|
|
).pack(side="left", padx=10)
|
|
tk.Button(
|
|
btn_frame,
|
|
text="OK",
|
|
command=dialog.destroy,
|
|
bg="#007aff",
|
|
fg="white",
|
|
relief="flat",
|
|
padx=20,
|
|
).pack(side="left", padx=10)
|
|
|
|
def _apply_ansible_topology_defaults(self):
|
|
info = _detect_ansible_topology(PROJECT_ROOT)
|
|
if not info:
|
|
return
|
|
self.ansible_topology = info
|
|
self.ansible_topology_summary = _format_ansible_topology_summary(info)
|
|
try:
|
|
net = self.prole_cfg_data.get("Network", {})
|
|
if info.get("topology_json"):
|
|
net["ANSIBLE_TOPOLOGY"] = info["topology_json"]
|
|
if info.get("inventory_path"):
|
|
net["ANSIBLE_INVENTORY"] = info["inventory_path"]
|
|
if info.get("infrastructure_path"):
|
|
net["ANSIBLE_INFRASTRUCTURE"] = info["infrastructure_path"]
|
|
if info.get("domain"):
|
|
net["ANSIBLE_DOMAIN"] = info["domain"]
|
|
if info.get("realm"):
|
|
net["ANSIBLE_REALM"] = info["realm"]
|
|
if info.get("ad_dc_host"):
|
|
net["AD_DC_HOST"] = info["ad_dc_host"]
|
|
if info.get("ad_dc_ip"):
|
|
net["AD_DC_IP"] = info["ad_dc_ip"]
|
|
if info.get("kdc_ip"):
|
|
net["KDC_ANSIBLE_DETECTED"] = info["kdc_ip"]
|
|
self.prole_cfg_data["Network"] = net
|
|
except Exception:
|
|
pass
|
|
|
|
try:
|
|
if not self.kerberos_kdc.get().strip() and info.get("kdc_ip"):
|
|
self.kerberos_kdc.set(info["kdc_ip"])
|
|
self.kerberos_enabled.set(True)
|
|
if not self.kerberos_realm.get().strip() and info.get("realm"):
|
|
self.kerberos_realm.set(info["realm"])
|
|
except Exception:
|
|
pass
|
|
|
|
def _read_k3s_cfg_values(self) -> tuple[str, str, str]:
|
|
"""Return (cluster_env, server_url, token) from prole.cfg if present.
|
|
|
|
Delegates to the shared ``_read_k3s_cfg`` so every presentation
|
|
layer (Tk, ncurses, silent) parses the same cfg sections.
|
|
"""
|
|
cfg_path = None
|
|
try:
|
|
if self._cfg_path_override is not None:
|
|
cfg_path = self._cfg_path_override
|
|
if cfg_path.is_dir():
|
|
cfg_path = cfg_path / "prole.cfg"
|
|
else:
|
|
cfg_path = self._resolve_prole_conf_dir() / "prole.cfg"
|
|
except Exception:
|
|
cfg_path = None
|
|
return _read_k3s_cfg(cfg_path)
|
|
|
|
def _apply_cluster_env_default_from_cfg(self):
|
|
cfg_env, _cfg_server, _cfg_token = self._read_k3s_cfg_values()
|
|
if not cfg_env:
|
|
return
|
|
try:
|
|
current = (self.cluster_env.get() or "").strip()
|
|
except Exception:
|
|
current = ""
|
|
if not current or current == "dev":
|
|
try:
|
|
self.cluster_env.set(_cluster_env_radio_value(cfg_env))
|
|
except Exception:
|
|
pass
|
|
self._set_deploy_target_from_cluster_env()
|
|
|
|
def _apply_k3s_defaults(self):
|
|
# Environment overrides
|
|
env_server = (
|
|
os.environ.get("PROLE_K3S_SERVER") or os.environ.get("K3S_SERVER_URL") or ""
|
|
).strip()
|
|
env_token = (
|
|
os.environ.get("PROLE_K3S_TOKEN") or os.environ.get("K3S_TOKEN") or ""
|
|
).strip()
|
|
env_token = _normalize_k3s_token(env_token)
|
|
if env_server and not self.k3s_server_url.get().strip():
|
|
self.k3s_server_url.set(env_server)
|
|
if env_token and not self.k3s_token.get().strip():
|
|
self.k3s_token.set(env_token)
|
|
|
|
# Config defaults
|
|
_cfg_env, cfg_server, cfg_token = self._read_k3s_cfg_values()
|
|
if cfg_server and not self.k3s_server_url.get().strip():
|
|
self.k3s_server_url.set(cfg_server)
|
|
if cfg_token and not self.k3s_token.get().strip():
|
|
self.k3s_token.set(cfg_token)
|
|
|
|
# Ansible defaults
|
|
info = getattr(self, "ansible_topology", None) or _detect_ansible_topology(
|
|
PROJECT_ROOT
|
|
)
|
|
if info:
|
|
if info.get("k3s_server_url") and not self.k3s_server_url.get().strip():
|
|
self.k3s_server_url.set(info["k3s_server_url"])
|
|
token_val = _normalize_k3s_token(info.get("k3s_token"))
|
|
if token_val and not self.k3s_token.get().strip():
|
|
self.k3s_token.set(token_val)
|