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>
864 lines
32 KiB
Python
864 lines
32 KiB
Python
"""
|
|
Prole Service Dependencies Installer — screen package.
|
|
|
|
This package splits the monolithic ``ProleInstaller`` class into cohesive
|
|
mixin modules, one per logical screen / concern. The composed class is
|
|
assembled here and re-exported so that external callers (``install.py``,
|
|
tests) see the same public surface as before.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import configparser
|
|
import platform
|
|
import signal
|
|
import socket
|
|
import threading
|
|
import tkinter as tk
|
|
import uuid
|
|
from tkinter import ttk, messagebox, filedialog
|
|
|
|
from installer import deploy as inst_deploy
|
|
from installer import screen as ui
|
|
from installer.build import get_build_command as inst_get_build_command
|
|
from installer.config import (
|
|
_expand_cfg_value,
|
|
_collect_cfg_vars,
|
|
_is_prole_secret,
|
|
_decrypt_prole_secret,
|
|
_is_openbao_ref,
|
|
_write_k3s_kubeconfig,
|
|
_encrypt_cfg_secret,
|
|
_encrypt_prole_secret,
|
|
)
|
|
from installer.core.controller import ProleController
|
|
from installer.core.env import (
|
|
_detect_ansible_topology,
|
|
_format_ansible_topology_summary,
|
|
_parse_ollama_host,
|
|
_normalize_k3s_token,
|
|
_deployment_target_label,
|
|
_cluster_env_radio_value,
|
|
_find_kubeconfig_file,
|
|
_bool_str,
|
|
_looks_like_k8s_bearer_token,
|
|
_deployment_mode_from_env,
|
|
_pf_extract_id,
|
|
_ensure_ansible_vault_credentials,
|
|
_openbao_placeholder,
|
|
_pf_upsert_mapping,
|
|
_format_ollama_host,
|
|
_default_opentofu_pipeline_url,
|
|
_build_required_port_forwards,
|
|
_render_prole_cfg,
|
|
_safe_str,
|
|
_normalize_cluster_env,
|
|
_k3d_prole_data_volume_args,
|
|
_push_docker_image,
|
|
_http_ping_registry,
|
|
_local_registry_enabled,
|
|
_collect_images_from_files,
|
|
)
|
|
|
|
# On macOS, set process name early so the menu bar reads correctly.
|
|
if platform.system() == "Darwin":
|
|
try:
|
|
from Foundation import NSProcessInfo
|
|
|
|
NSProcessInfo.processInfo().setProcessName_("Prole Database Installer")
|
|
except Exception:
|
|
pass
|
|
|
|
from installer.core.env import * # noqa: F401,F403
|
|
from installer.core.actions import (
|
|
ProleInstallerBase,
|
|
ProleSilentInstaller,
|
|
_prepare_k3s_pipeline,
|
|
_reset_k3s_namespace,
|
|
)
|
|
|
|
# Keep global frames list in memory (legacy requirement).
|
|
GLOBAL_SCAN_FRAMES: list = []
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Mixin imports — one per screen / concern
|
|
# ---------------------------------------------------------------------------
|
|
from installer.ui.screens.base import ScreenBaseMixin
|
|
from installer.ui.screens.navigation import NavigationMixin
|
|
from installer.ui.screens.welcome import WelcomeScreenMixin
|
|
from installer.ui.screens.dependencies import DependenciesScreenMixin
|
|
from installer.ui.screens.network import NetworkScreenMixin
|
|
from installer.ui.screens.environment import EnvironmentScreenMixin
|
|
from installer.ui.screens.database import DatabaseScreenMixin
|
|
from installer.ui.screens.database_options import DatabaseOptionsScreenMixin
|
|
from installer.ui.screens.cluster import ClusterScreenMixin
|
|
from installer.ui.screens.services import ServicesScreenMixin
|
|
from installer.ui.screens.security import SecurityScreenMixin
|
|
from installer.ui.screens.ollama import OllamaScreenMixin
|
|
from installer.ui.screens.supabase import SupabaseScreenMixin
|
|
from installer.ui.screens.gitops import GitOpsScreenMixin
|
|
from installer.ui.screens.docker import DockerScreenMixin
|
|
from installer.ui.screens.build import BuildScreenMixin
|
|
from installer.ui.screens.packaging import PackagingScreenMixin
|
|
from installer.ui.screens.deploy import DeployScreenMixin
|
|
from installer.ui.screens.validate import ValidateScreenMixin
|
|
from installer.ui.screens.cfg import ConfigMixin
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Composed installer class
|
|
# ---------------------------------------------------------------------------
|
|
class ProleInstaller(
|
|
ProleInstallerBase,
|
|
ScreenBaseMixin,
|
|
NavigationMixin,
|
|
WelcomeScreenMixin,
|
|
DependenciesScreenMixin,
|
|
NetworkScreenMixin,
|
|
EnvironmentScreenMixin,
|
|
DatabaseScreenMixin,
|
|
DatabaseOptionsScreenMixin,
|
|
ClusterScreenMixin,
|
|
ServicesScreenMixin,
|
|
SecurityScreenMixin,
|
|
OllamaScreenMixin,
|
|
SupabaseScreenMixin,
|
|
GitOpsScreenMixin,
|
|
DockerScreenMixin,
|
|
BuildScreenMixin,
|
|
PackagingScreenMixin,
|
|
DeployScreenMixin,
|
|
ValidateScreenMixin,
|
|
ConfigMixin,
|
|
):
|
|
"""Prole GUI installer — assembled from per-screen mixins."""
|
|
|
|
KUBECTL_STATUS_TAB = "__kubectl_status__"
|
|
|
|
# ---- Data access bridge (satisfies ProleInstallerBase contract) ----
|
|
_INPUT_VAR_MAP: dict[str, str] = {
|
|
"init_cluster.cluster_env": "cluster_env",
|
|
"init_password.db_namespace": "db_namespace",
|
|
"init_password.db_username": "db_username",
|
|
"init_password.db_password": "db_password",
|
|
"init_password.db_host_port": "db_host_port",
|
|
"env_setup.NAMESPACE": "db_namespace",
|
|
"kerberos_config.enabled": "kerberos_enabled",
|
|
"kerberos_config.realm": "kerberos_realm",
|
|
"kerberos_config.kdc": "kerberos_kdc",
|
|
"kerberos_config.user": "kerberos_user",
|
|
"kerberos_config.password": "kerberos_password",
|
|
"init_cluster.kerberos_enabled": "kerberos_enabled",
|
|
"init_cluster.supabase_enabled": "supabase_enabled",
|
|
"init_cluster.gitops_enabled": "gitops_enabled",
|
|
"init_cluster.at_rest_encryption_enabled": "at_rest_encryption_enabled",
|
|
"init_cluster.selected_kubectx": "selected_kubectx",
|
|
"init_cluster.k3s_server_url": "k3s_server_url",
|
|
"init_cluster.k3s_token": "k3s_token",
|
|
"ollama_config.server_host": "ollama_server_host",
|
|
"ollama_config.server_port": "ollama_server_port",
|
|
"ollama_config.model": "ollama_model",
|
|
"gitops.namespace": "gitops_namespace",
|
|
}
|
|
|
|
def _get_input(self, key: str, default: str | None = None) -> str:
|
|
"""Bridge for :class:`ProleInstallerBase` — reads tk vars by key."""
|
|
if key.startswith("env_setup.") and hasattr(self, "_env_entries"):
|
|
env_key = key[len("env_setup.") :]
|
|
ent = self._env_entries.get(env_key)
|
|
if ent is not None:
|
|
try:
|
|
val = _safe_str(ent.get().strip())
|
|
if val:
|
|
return val
|
|
except Exception:
|
|
pass
|
|
attr = self._INPUT_VAR_MAP.get(key)
|
|
if attr:
|
|
var = getattr(self, attr, None)
|
|
if var is not None:
|
|
try:
|
|
val = var.get()
|
|
if hasattr(val, "__class__") and "MagicMock" in val.__class__.__name__:
|
|
return default if default is not None else ""
|
|
if isinstance(val, bool):
|
|
return "true" if val else "false"
|
|
return _safe_str(val)
|
|
except Exception:
|
|
pass
|
|
return default if default is not None else ""
|
|
|
|
# ------------------------------------------------------------------ init
|
|
def __init__(
|
|
self,
|
|
root,
|
|
config_path: str | None = None,
|
|
reset_cluster: bool = False,
|
|
verbose: bool = False,
|
|
):
|
|
# Resolve config path early for the controller
|
|
resolved_cfg = None
|
|
if config_path:
|
|
try:
|
|
resolved_cfg = Path(config_path).expanduser()
|
|
except Exception:
|
|
pass
|
|
if not resolved_cfg:
|
|
resolved_cfg = PROJECT_ROOT / "conf" / "prole.cfg"
|
|
|
|
self.controller = ProleController(
|
|
PROJECT_ROOT, verbose=verbose, cfg_path=resolved_cfg
|
|
)
|
|
self.project_root = PROJECT_ROOT
|
|
self.cfg_path = resolved_cfg
|
|
self._init_shared_state()
|
|
self._init_database_options_state()
|
|
self.screens = None
|
|
self.deploy_environment = None
|
|
self.root = root
|
|
self._cfg_path_override = None
|
|
if config_path:
|
|
try:
|
|
self._cfg_path_override = Path(config_path).expanduser()
|
|
except Exception:
|
|
self._cfg_path_override = None
|
|
self._installing_dep_id = None
|
|
self._managed_kubeconfig = None
|
|
self._kong_last_repair_at = 0.0
|
|
self._kong_repair_cooldown_s = 120
|
|
self._repair_last_run_at = 0.0
|
|
self._repair_cooldown_s = 600
|
|
self._repair_inflight = False
|
|
self._reset_cluster = bool(reset_cluster)
|
|
_ensure_ansible_vault_credentials(prompt_ui=True, root=root)
|
|
self._action_flags = {}
|
|
self.root.title("Prole Database Installer")
|
|
try:
|
|
self._set_app_identity()
|
|
except Exception:
|
|
pass
|
|
|
|
# Center window on screen
|
|
window_width = 1300
|
|
window_height = 910
|
|
screen_width = root.winfo_screenwidth()
|
|
screen_height = root.winfo_screenheight()
|
|
center_x = int(screen_width / 2 - window_width / 2)
|
|
center_y = int(screen_height / 2 - window_height / 2)
|
|
self.root.geometry(f"{window_width}x{window_height}+{center_x}+{center_y}")
|
|
self.root.resizable(False, False)
|
|
|
|
# Force light mode colors
|
|
self.root.configure(bg="white")
|
|
|
|
# Style configuration
|
|
self.style = ttk.Style()
|
|
self.configure_styles()
|
|
|
|
# Main container with two columns
|
|
self.main_container = tk.Frame(root, bg="white")
|
|
self.main_container.pack(fill="both", expand=True)
|
|
|
|
# Left Sidebar
|
|
self.sidebar = tk.Frame(self.main_container, bg="#F5F5DC", width=325)
|
|
self.sidebar.pack(side="left", fill="y")
|
|
self.sidebar.pack_propagate(False)
|
|
|
|
# Vertical Divider Line
|
|
self.divider = tk.Frame(
|
|
self.main_container, bg="#CCCCCC", width=1, highlightthickness=0, bd=0
|
|
)
|
|
self.divider.pack(side="left", fill="y")
|
|
|
|
# Right Content Area
|
|
self.content_area = tk.Frame(self.main_container, bg="white")
|
|
self.content_area.pack(side="left", fill="both", expand=True)
|
|
|
|
# Footer for Next/Prev buttons
|
|
btns = ui.create_nav_footer(
|
|
self.content_area,
|
|
buttons=[(1, "Previous"), (2, "Next"), (3, "Deploy"), (4, "Launch")],
|
|
commands={
|
|
1: self.on_prev,
|
|
2: self.on_next,
|
|
3: self.on_deploy,
|
|
4: self.on_launch,
|
|
},
|
|
style_name="Nav.TButton",
|
|
gear_command=self.show_config_dialog,
|
|
)
|
|
self.footer = btns.get("_footer")
|
|
self.prev_button = btns.get(1)
|
|
self.next_button = btns.get(2)
|
|
self.deploy_button = btns.get(3)
|
|
self.launch_button = btns.get(4)
|
|
self.gear_button = btns.get("gear")
|
|
|
|
# Background canvas
|
|
self._bg_pil = None
|
|
self._bg_tk = None
|
|
self.bg_canvas = tk.Canvas(
|
|
self.content_area, highlightthickness=0, bd=0, bg="white"
|
|
)
|
|
self.bg_canvas.pack(fill="both", expand=True)
|
|
self._bg_item = None
|
|
|
|
# Slide area overlay
|
|
self.slide_area = tk.Frame(self.bg_canvas, bg="white")
|
|
self.slide_area.place(relx=0, rely=0, relwidth=1, relheight=1)
|
|
self.slide_area.lower()
|
|
|
|
try:
|
|
from PIL import Image, ImageTk
|
|
|
|
bg_path = get_resource_path("img/proleLogoSepia.png")
|
|
if bg_path.exists():
|
|
original = Image.open(str(bg_path)).convert("RGBA")
|
|
white_bg = Image.new("RGBA", original.size, (255, 255, 255, 255))
|
|
self._bg_pil = Image.blend(white_bg, original, 0.15)
|
|
|
|
def _render_bg(event=None):
|
|
if not self._bg_pil:
|
|
return
|
|
cw = max(1, self.bg_canvas.winfo_width())
|
|
ch = max(1, self.bg_canvas.winfo_height())
|
|
iw, ih = self._bg_pil.size
|
|
scale = max(cw / iw, ch / ih)
|
|
nw, nh = max(1, int(iw * scale)), max(1, int(ih * scale))
|
|
img = self._bg_pil.resize((nw, nh), Image.LANCZOS)
|
|
self._bg_tk = ImageTk.PhotoImage(img)
|
|
if self._bg_item is not None:
|
|
self.bg_canvas.delete(self._bg_item)
|
|
self._bg_item = self.bg_canvas.create_image(
|
|
cw // 2, ch // 2, image=self._bg_tk, anchor="center"
|
|
)
|
|
self.bg_canvas.tag_lower(self._bg_item)
|
|
|
|
self.bg_canvas.bind("<Configure>", _render_bg)
|
|
except Exception as e:
|
|
print(f"Error loading background: {e}")
|
|
|
|
# Navigation menu items
|
|
self.nav_items = [
|
|
("Welcome", "welcome"),
|
|
("Dependencies", "deps_summary"),
|
|
("Network", "network_scan"),
|
|
("System Environment", "env_setup"),
|
|
("Cluster Environment", "init_cluster"),
|
|
("Database Options", "database_options"),
|
|
("Docker Build", "init_db_build"),
|
|
("Database Creation", "init_password"),
|
|
("Initialization Scripts", "init_scripts"),
|
|
("Kerberos Authentication", "kerberos_config"),
|
|
("GitOps", "gitops_config"),
|
|
("Supabase", "supabase_config"),
|
|
("Ollama", "ollama_config"),
|
|
("Deployment", "init_cnpg_deploy"),
|
|
("Post Install", "create_installer"),
|
|
]
|
|
self.nav_widgets = {}
|
|
self._create_sidebar_nav()
|
|
|
|
# Validation attributes
|
|
self.validation_running = False
|
|
self.validation_thread = None
|
|
self._welcome_scan_started = False
|
|
self.splash_scan_running = False
|
|
self.splash_scan_done_at = None
|
|
self.splash_scan_started_at = None
|
|
self._splash_status_item = None
|
|
try:
|
|
self.expected_host = (platform.node() or socket.gethostname()).split(".")[0]
|
|
except Exception:
|
|
self.expected_host = None
|
|
|
|
# Canvas page state
|
|
self._canvas_page = None
|
|
self._canvas_items = []
|
|
self.canvas_renderers = {}
|
|
self._overlay_widgets = []
|
|
self._cursor_blink_after_id = None
|
|
self._cursor_blink_visible = False
|
|
self._k3s_status_after_id = None
|
|
self._k3s_status_active = False
|
|
self._k3s_status_inflight = False
|
|
self._k3s_service_status_console = None
|
|
self._k3s_service_deploy_console = None
|
|
self._k3s_service_notebook = None
|
|
self._k3s_service_status_tab = None
|
|
self._k3s_service_deploy_tab = None
|
|
self._common_services_success = False
|
|
self._all_services_green = False
|
|
self._showing_service_overlay = False
|
|
self._service_traffic_lights = {}
|
|
self._k3s_deploy_btn = None
|
|
self._last_registry_log_path = None
|
|
self._last_common_services_log_path = None
|
|
|
|
# Shared dependency catalog
|
|
self.dependencies = list(inst_config.DEPENDENCIES)
|
|
|
|
# Docker import directory
|
|
self.docker_import_dir = tk.StringVar()
|
|
|
|
# Initialize prole.cfg data (override base with tk-aware version)
|
|
self.prole_cfg_data = {
|
|
"Global": {},
|
|
"Welcome": {},
|
|
"Dependencies": {},
|
|
"Network": {},
|
|
"Port Forwards": {},
|
|
"System Environment": {},
|
|
"Kerberos Authentication": {},
|
|
"Ollama": {},
|
|
"Optional Features": {},
|
|
"Database Creation": {},
|
|
"Docker Build": {},
|
|
"Initialize Cluster": {},
|
|
"Initialization Scripts": {},
|
|
"GitOps": {},
|
|
"Deployment": {},
|
|
"Dev Cluster (k3d)": {},
|
|
"Service Cluster (k3s)": {},
|
|
"Prod Cluster (k8s)": {},
|
|
"Install": {},
|
|
}
|
|
self._cfg_secret_cache = {}
|
|
self._secrets_finalized = False
|
|
self.service_namespace = tk.StringVar(value="default")
|
|
|
|
# Load existing docker_import_dir from prole.cfg
|
|
try:
|
|
conf_dir = self._resolve_prole_conf_dir()
|
|
cfg_path = conf_dir / "prole.cfg"
|
|
if self._cfg_path_override:
|
|
cfg_path = self._cfg_path_override
|
|
if cfg_path.exists():
|
|
cfg = configparser.ConfigParser(interpolation=None)
|
|
cfg.optionxform = str
|
|
cfg.read(cfg_path)
|
|
cfg_vars = _collect_cfg_vars(cfg)
|
|
if cfg.has_section("Global"):
|
|
if "DOCKER_IMPORT_DIR" in cfg["Global"]:
|
|
docker_dir = _expand_cfg_value(
|
|
cfg["Global"]["DOCKER_IMPORT_DIR"], cfg_vars
|
|
)
|
|
self.docker_import_dir.set(docker_dir)
|
|
self.prole_cfg_data["Global"]["DOCKER_IMPORT_DIR"] = docker_dir
|
|
saved_cluster = _expand_cfg_value(
|
|
cfg["Global"].get("CLUSTER_ENV", ""), cfg_vars
|
|
).strip()
|
|
saved_ns = _expand_cfg_value(
|
|
cfg["Global"].get("NAMESPACE", ""), cfg_vars
|
|
).strip()
|
|
saved_service_ns = _expand_cfg_value(
|
|
cfg["Global"].get("SERVICE_NAMESPACE", ""), cfg_vars
|
|
).strip()
|
|
saved_gitops_ns = (
|
|
_expand_cfg_value(
|
|
cfg.get("GitOps", "GITOPS_NAMESPACE", fallback=""),
|
|
cfg_vars,
|
|
).strip()
|
|
if cfg.has_section("GitOps")
|
|
else ""
|
|
)
|
|
saved_gitops_enabled = (
|
|
_expand_cfg_value(
|
|
cfg.get(
|
|
"Optional Features",
|
|
"GITOPS_ENABLED",
|
|
fallback="",
|
|
),
|
|
cfg_vars,
|
|
).strip()
|
|
if cfg.has_section("Optional Features")
|
|
else ""
|
|
)
|
|
if saved_cluster:
|
|
try:
|
|
self.cluster_env.set(saved_cluster)
|
|
except Exception:
|
|
pass
|
|
if saved_ns:
|
|
try:
|
|
self.db_namespace.set(saved_ns)
|
|
except Exception:
|
|
pass
|
|
try:
|
|
self.prole_cfg_data.setdefault("Global", {})[
|
|
"NAMESPACE"
|
|
] = saved_ns
|
|
except Exception:
|
|
pass
|
|
if saved_service_ns:
|
|
try:
|
|
self.service_namespace.set(saved_service_ns)
|
|
except Exception:
|
|
pass
|
|
self.prole_cfg_data["Global"][
|
|
"SERVICE_NAMESPACE"
|
|
] = saved_service_ns
|
|
if saved_gitops_ns:
|
|
try:
|
|
self.gitops_namespace.set(saved_gitops_ns)
|
|
except Exception:
|
|
pass
|
|
self.prole_cfg_data.setdefault("GitOps", {})[
|
|
"NAMESPACE"
|
|
] = saved_gitops_ns
|
|
if saved_gitops_enabled:
|
|
val = saved_gitops_enabled.lower() in ("true", "1", "yes")
|
|
try:
|
|
self.gitops_enabled.set(val)
|
|
except Exception:
|
|
pass
|
|
self.prole_cfg_data.setdefault("Optional Features", {})[
|
|
"GITOPS_ENABLED"
|
|
] = str(val)
|
|
except Exception:
|
|
pass
|
|
self.prole_cfg_data["Global"].setdefault(
|
|
"SERVICE_NAMESPACE", self.service_namespace.get()
|
|
)
|
|
|
|
# Disk selection variables
|
|
self.selected_disk_type = tk.StringVar(value="local")
|
|
self.selected_removable_disk = tk.StringVar()
|
|
self.selected_local_path = tk.StringVar(value=str(Path.home()))
|
|
self.removable_disks = []
|
|
|
|
# Wizard pages setup
|
|
self.pages = []
|
|
self.page_index = 0
|
|
|
|
# Cluster / k3d variables
|
|
self.cluster_env = tk.StringVar(value="dev")
|
|
self.k3d_cluster_name = tk.StringVar(value="knoe-dev-cluster")
|
|
self.k3d_cluster_list = tk.Variable(value=[])
|
|
self.selected_k3d_cluster = tk.StringVar()
|
|
try:
|
|
cfg_env = (self.prole_cfg_data.get("Initialize Cluster", {}) or {}).get(
|
|
"ENVIRONMENT", ""
|
|
)
|
|
if _normalize_cluster_env(cfg_env) == "dev":
|
|
self.selected_k3d_cluster.set(cfg_env)
|
|
except Exception:
|
|
pass
|
|
|
|
self.deploy_target = tk.StringVar(
|
|
value=_deployment_target_label(self.cluster_env.get())
|
|
)
|
|
self._syncing_deploy_target = False
|
|
self._deploy_target_trace = self.deploy_target.trace_add(
|
|
"write", self._on_deploy_target_change
|
|
)
|
|
self.kubectx_list = tk.Variable(value=self._get_kubectx_list())
|
|
self.selected_kubectx = tk.StringVar()
|
|
self.prod_artifacts_path = tk.StringVar(
|
|
value=str(PROJECT_ROOT / "data" / "staging")
|
|
)
|
|
self.k3s_server_url = tk.StringVar()
|
|
self.k3s_token = tk.StringVar()
|
|
self.k3s_services_status = {
|
|
"registry": tk.StringVar(value="Unknown"),
|
|
"openbao": tk.StringVar(value="Unknown"),
|
|
"opentofu": tk.StringVar(value="Unknown"),
|
|
}
|
|
try:
|
|
self.db_username = tk.StringVar(value=os.getlogin())
|
|
except Exception:
|
|
self.db_username = tk.StringVar(value="prole")
|
|
self.db_password = tk.StringVar()
|
|
self.db_password_confirm = tk.StringVar()
|
|
self.namespace_owner = self._get_local_owner()
|
|
self.db_namespace = tk.StringVar(value=self._initial_namespace())
|
|
try:
|
|
_ns_from_cfg = (
|
|
(self.prole_cfg_data.get("Global", {}) or {})
|
|
.get("NAMESPACE", "")
|
|
.strip()
|
|
)
|
|
if _ns_from_cfg:
|
|
self.db_namespace.set(self._ensure_namespace_prefix(_ns_from_cfg))
|
|
except Exception:
|
|
pass
|
|
self.db_host_port = tk.StringVar(value="5432")
|
|
self.db_namespace_suffix = tk.StringVar()
|
|
self._updating_namespace_fields = False
|
|
self._sync_namespace_suffix_from_full()
|
|
self.db_namespace.trace_add(
|
|
"write", lambda *args: self._sync_namespace_suffix_from_full()
|
|
)
|
|
self.db_namespace_suffix.trace_add(
|
|
"write", lambda *args: self._sync_namespace_full_from_suffix()
|
|
)
|
|
|
|
# Kerberos / optional features
|
|
self.kerberos_enabled = tk.BooleanVar(value=False)
|
|
self.kerberos_realm = tk.StringVar()
|
|
self.kerberos_user = tk.StringVar(value="administrator")
|
|
self.kerberos_password = tk.StringVar()
|
|
self.kerberos_kdc = tk.StringVar()
|
|
self.supabase_enabled = tk.BooleanVar(value=False)
|
|
self.gitops_enabled = tk.BooleanVar(value=False)
|
|
self.at_rest_encryption_enabled = tk.BooleanVar(value=True)
|
|
self.gitops_namespace = tk.StringVar(value="gitea")
|
|
self.ollama_server_host = tk.StringVar()
|
|
self.ollama_server_port = tk.StringVar(value=DEFAULT_OLLAMA_PORT)
|
|
self.ollama_model = tk.StringVar()
|
|
self._ollama_servers_cache = []
|
|
self._ollama_model_options = {}
|
|
self.ansible_topology = {}
|
|
self.ansible_topology_summary = ""
|
|
try:
|
|
self._apply_ansible_topology_defaults()
|
|
except Exception:
|
|
pass
|
|
|
|
# Load encrypted secrets, apply cfg defaults
|
|
try:
|
|
self._load_secret_cache_from_cfg()
|
|
except Exception:
|
|
pass
|
|
try:
|
|
self._apply_cluster_env_default_from_cfg()
|
|
except Exception:
|
|
pass
|
|
try:
|
|
self._apply_k3s_defaults()
|
|
except Exception:
|
|
pass
|
|
try:
|
|
self._apply_ollama_defaults()
|
|
except Exception:
|
|
pass
|
|
|
|
# Create pages
|
|
self.page_frames = {}
|
|
self.verify_mode = tk.BooleanVar(value=False)
|
|
|
|
# Register canvas renderers
|
|
self._register_canvas_renderer("welcome", self._render_welcome_page)
|
|
self._register_canvas_renderer("network_scan", self._render_network_scan_page)
|
|
self._register_canvas_renderer("env_setup", self._render_env_setup_page)
|
|
self._register_canvas_renderer("deps_summary", self._render_deps_summary_page)
|
|
for dep in self.dependencies:
|
|
self._register_canvas_renderer(
|
|
f"dep_{dep['id']}", lambda d=dep: self._render_dependency_page(d)
|
|
)
|
|
self._register_canvas_renderer(
|
|
"kerberos_config", self._render_kerberos_config_page
|
|
)
|
|
self._register_canvas_renderer("ollama_config", self._render_ollama_config_page)
|
|
self._register_canvas_renderer(
|
|
"supabase_config", self._render_supabase_config_page
|
|
)
|
|
self._register_canvas_renderer("gitops_config", self._render_gitops_config_page)
|
|
self._register_canvas_renderer("init_cluster", self._render_init_cluster_page)
|
|
self._register_canvas_renderer("init_db_build", self._render_init_db_build_page)
|
|
self._register_canvas_renderer("database_options", self._render_database_options_page)
|
|
self._register_canvas_renderer("init_password", self._render_init_password_page)
|
|
self._register_canvas_renderer(
|
|
"init_cnpg_deploy", self._render_init_cnpg_deploy_page
|
|
)
|
|
self._register_canvas_renderer("init_scripts", self._render_init_scripts_page)
|
|
self._register_canvas_renderer("build", self._render_build_page)
|
|
self._register_canvas_renderer(
|
|
"disk_selection", self._render_disk_selection_page
|
|
)
|
|
self._register_canvas_renderer("build_summary", self._render_build_summary_page)
|
|
self._register_canvas_renderer(
|
|
"create_installer", self._render_create_installer_page
|
|
)
|
|
|
|
# Page ordering for navigation
|
|
self._register_page("welcome", None)
|
|
self._register_page("deps_summary", None)
|
|
for dep in self.dependencies:
|
|
self._register_page(f"dep_{dep['id']}", None)
|
|
self._register_page("network_scan", None)
|
|
self._register_page("env_setup", None)
|
|
self._register_page("init_cluster", None)
|
|
self._register_page("database_options", None)
|
|
self._register_page("init_password", None)
|
|
self._register_page("init_db_build", None)
|
|
self._register_page("init_scripts", None)
|
|
self._register_page("kerberos_config", None)
|
|
self._register_page("gitops_config", None)
|
|
self._register_page("supabase_config", None)
|
|
self._register_page("ollama_config", None)
|
|
self._register_page("init_cnpg_deploy", None)
|
|
self._register_page("build", None)
|
|
self._register_page("disk_selection", None)
|
|
self._register_page("build_summary", None)
|
|
self._register_page("create_installer", None)
|
|
|
|
# Initial render
|
|
self.root.after(
|
|
100, lambda: self.selected_local_path.set(str(self._get_prole_dist_dir()))
|
|
)
|
|
self.update_footer()
|
|
self.show_page(0)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Module-level helpers (kept for backward compatibility)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def has_display():
|
|
"""Check if a display is available for GUI."""
|
|
if os.environ.get("DISPLAY"):
|
|
return True
|
|
if platform.system() == "Darwin":
|
|
try:
|
|
test_root = tk.Tk()
|
|
test_root.withdraw()
|
|
test_root.destroy()
|
|
return True
|
|
except Exception:
|
|
return False
|
|
return False
|
|
|
|
|
|
def main():
|
|
import argparse
|
|
|
|
parser = argparse.ArgumentParser(description="Prole Database Installer")
|
|
parser.add_argument(
|
|
"-c",
|
|
"--config",
|
|
default=None,
|
|
help="Path to prole.cfg for saving or silent replay",
|
|
)
|
|
parser.add_argument(
|
|
"-s",
|
|
"-S",
|
|
"--silent",
|
|
action="store_true",
|
|
help="Run unattended install in console mode using prole.cfg",
|
|
)
|
|
parser.add_argument(
|
|
"-l",
|
|
"--log",
|
|
nargs="?",
|
|
const=True,
|
|
default=None,
|
|
help="Enable logging to file. Optionally provide a path (default: /tmp/prole_install_output.log)",
|
|
)
|
|
parser.add_argument(
|
|
"--prepare-k3s-pipeline",
|
|
action="store_true",
|
|
help="Prepare OpenTofu k3s pipeline and run silent install test",
|
|
)
|
|
parser.add_argument(
|
|
"--reset",
|
|
action="store_true",
|
|
help="Reset k3d dev cluster or k3s service cluster",
|
|
)
|
|
parser.add_argument(
|
|
"--no-gui",
|
|
action="store_true",
|
|
help="Run installer with ncurses terminal interface",
|
|
)
|
|
parser.add_argument("--gui", action="store_true", help="Force GUI mode")
|
|
parser.add_argument(
|
|
"-v", "--verbose", action="store_true", help="Enable verbose output"
|
|
)
|
|
parser.add_argument(
|
|
"-d", "--debug", action="store_true", help="Enable debug logging"
|
|
)
|
|
parser.add_argument(
|
|
"command", nargs="?", default=None, help="Subcommand to run (e.g. monitor)"
|
|
)
|
|
parser.add_argument("command_args", nargs="*", help="Arguments for the subcommand")
|
|
args = parser.parse_args()
|
|
|
|
# Set up log file tee if --log is requested
|
|
log_file_path = None
|
|
if args.log is not None:
|
|
if args.log is True:
|
|
import tempfile
|
|
|
|
log_file_path = "/tmp/prole_install_output.log"
|
|
else:
|
|
log_file_path = args.log
|
|
|
|
from installer.config import setup_logging
|
|
|
|
setup_logging(verbose=args.verbose, debug=args.debug)
|
|
|
|
controller = ProleController(
|
|
PROJECT_ROOT,
|
|
verbose=args.verbose,
|
|
cfg_path=Path(args.config).expanduser() if args.config else None,
|
|
)
|
|
|
|
if args.command == "monitor":
|
|
from installer.core.monitor import run_monitor
|
|
|
|
pf_cfg = args.command_args[0] if args.command_args else None
|
|
sys.exit(run_monitor(controller, args.config, pf_cfg, verbose=args.verbose))
|
|
|
|
if args.silent:
|
|
_ensure_ansible_vault_credentials(prompt_ui=False)
|
|
installer = ProleSilentInstaller(
|
|
controller, args.config, reset_cluster=args.reset
|
|
)
|
|
if log_file_path:
|
|
installer.enable_log_file(log_file_path)
|
|
rc = installer.run()
|
|
if rc != 0:
|
|
sys.exit(rc)
|
|
if args.prepare_k3s_pipeline:
|
|
rc = _prepare_k3s_pipeline(controller, skip_validation=True)
|
|
sys.exit(rc)
|
|
|
|
if args.prepare_k3s_pipeline:
|
|
rc = _prepare_k3s_pipeline(controller)
|
|
sys.exit(rc)
|
|
|
|
use_gui = False
|
|
if args.gui:
|
|
use_gui = True
|
|
elif args.no_gui:
|
|
use_gui = False
|
|
else:
|
|
use_gui = has_display()
|
|
|
|
if use_gui:
|
|
try:
|
|
root = tk.Tk()
|
|
root.title("Prole Database Installer")
|
|
window_width, window_height = 1000, 700
|
|
try:
|
|
sw, sh = root.winfo_screenwidth(), root.winfo_screenheight()
|
|
cx, cy = int(sw / 2 - window_width / 2), int(sh / 2 - window_height / 2)
|
|
root.geometry(f"{window_width}x{window_height}+{cx}+{cy}")
|
|
except Exception:
|
|
root.geometry(f"{window_width}x{window_height}")
|
|
_ensure_ansible_vault_credentials(prompt_ui=True, root=root)
|
|
ProleInstaller(
|
|
root,
|
|
config_path=args.config,
|
|
reset_cluster=args.reset,
|
|
verbose=args.verbose,
|
|
)
|
|
root.mainloop()
|
|
except Exception as e:
|
|
print(f"Failed to start GUI: {e}", file=sys.stderr)
|
|
print("Falling back to ncurses interface...", file=sys.stderr)
|
|
time.sleep(1)
|
|
from installer.ncurses_installer import run_ncurses_installer
|
|
|
|
run_ncurses_installer(controller)
|
|
else:
|
|
_ensure_ansible_vault_credentials(prompt_ui=False)
|
|
from installer.ncurses_installer import run_ncurses_installer
|
|
|
|
run_ncurses_installer(controller)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|