mirror of
https://github.com/dredx/prole.git
synced 2026-09-23 10:13:58 +00:00
1322 lines
53 KiB
Python
1322 lines
53 KiB
Python
"""
|
|
Knoe Service Dependencies Installer — screen package.
|
|
|
|
This package splits the monolithic ``KnoeInstaller`` 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 signal
|
|
import socket
|
|
import sys
|
|
import threading
|
|
import time
|
|
import tkinter as tk
|
|
import uuid
|
|
from tkinter import ttk, messagebox, filedialog
|
|
|
|
import platform
|
|
from knoe import screen as ui
|
|
from knoe.config import (
|
|
_expand_cfg_value,
|
|
_collect_cfg_vars,
|
|
_parse_gcp_cfg,
|
|
_preserve_cfg_expr_for_persistence,
|
|
)
|
|
from knoe.core.controller import KnoeController
|
|
from knoe.core.env import (
|
|
_deployment_target_label,
|
|
_safe_str,
|
|
_normalize_cluster_env,
|
|
)
|
|
from knoe.core.onepassword import ensure_op_signed_in, ensure_knoey_vault
|
|
|
|
# Helper to set macOS application identity via Objective-C bridge.
|
|
def setup_macos_app_identity():
|
|
"""Set the installer identity: process/menu name on macOS.
|
|
This should be called AFTER the Tk root window is initialized.
|
|
"""
|
|
if platform.system() != "Darwin":
|
|
return
|
|
|
|
try:
|
|
import ctypes
|
|
import ctypes.util
|
|
|
|
objc = ctypes.cdll.LoadLibrary(ctypes.util.find_library("objc"))
|
|
objc.objc_getClass.restype = ctypes.c_void_p
|
|
objc.sel_registerName.restype = ctypes.c_void_p
|
|
objc.objc_msgSend.restype = ctypes.c_void_p
|
|
|
|
def msg(obj, sel_name, *args, restype=ctypes.c_void_p, argtypes=None):
|
|
if obj is None:
|
|
return None
|
|
sel = objc.sel_registerName(sel_name.encode("ascii"))
|
|
# Use argtypes to ensure correct calling convention
|
|
objc.objc_msgSend.restype = restype
|
|
if argtypes is not None:
|
|
objc.objc_msgSend.argtypes = [ctypes.c_void_p, ctypes.c_void_p] + argtypes
|
|
else:
|
|
objc.objc_msgSend.argtypes = [ctypes.c_void_p, ctypes.c_void_p]
|
|
return objc.objc_msgSend(obj, sel, *args)
|
|
|
|
def to_ns(py_str):
|
|
NSUTF8StringEncoding = 4
|
|
cls = objc.objc_getClass(b"NSString")
|
|
return msg(
|
|
cls,
|
|
"stringWithCString:encoding:",
|
|
py_str.encode("utf-8"),
|
|
NSUTF8StringEncoding,
|
|
argtypes=[ctypes.c_char_p, ctypes.c_uint],
|
|
)
|
|
|
|
app_name_str = "Knoe.DB Installer"
|
|
app_name_ns = to_ns(app_name_str)
|
|
|
|
# 1. Set process name
|
|
process_info_cls = objc.objc_getClass(b"NSProcessInfo")
|
|
if process_info_cls:
|
|
process_info = msg(process_info_cls, "processInfo")
|
|
if process_info:
|
|
# setProcessName: is often ignored for bundled apps but helps for scripts.
|
|
msg(process_info, "setProcessName:", app_name_ns, argtypes=[ctypes.c_void_p])
|
|
|
|
# 1a. Attempt to set process name via libc's setprogname
|
|
try:
|
|
libc = ctypes.cdll.LoadLibrary(ctypes.util.find_library("c"))
|
|
if hasattr(libc, "setprogname"):
|
|
libc.setprogname.argtypes = [ctypes.c_char_p]
|
|
libc.setprogname(app_name_str.encode("utf-8"))
|
|
except Exception:
|
|
pass
|
|
|
|
# 1b. Manipulate argv[0] as a fallback for some systems
|
|
try:
|
|
# sys.argv[0] doesn't change the OS process name but can affect some libraries
|
|
import sys
|
|
sys.argv[0] = app_name_str
|
|
except Exception:
|
|
pass
|
|
|
|
# 1b. Try to set Dock name via LSSetApplicationInformationItem (SPI)
|
|
# This is a bit risky but often works for setting the Dock mouseover name at runtime.
|
|
try:
|
|
LSSetApplicationInformationItem = getattr(objc, "_LSSetApplicationInformationItem", None)
|
|
if LSSetApplicationInformationItem:
|
|
# kLSKeyAllFrontProcess = -2
|
|
# kLSRuntimeApplicationNameKey = "LSRuntimeApplicationName"
|
|
# _LSSetApplicationInformationItem(int, int, CFStringRef, CFStringRef, int*)
|
|
LSSetApplicationInformationItem.argtypes = [ctypes.c_int, ctypes.c_int, ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p]
|
|
LSSetApplicationInformationItem.restype = ctypes.c_int
|
|
|
|
LSRuntimeApplicationNameKey = to_ns("LSRuntimeApplicationName")
|
|
LSSetApplicationInformationItem(-2, -1, LSRuntimeApplicationNameKey, app_name_ns, None)
|
|
except Exception:
|
|
pass
|
|
|
|
# 2. Set transformation state and Dock Icon
|
|
app_cls = objc.objc_getClass(b"NSApplication")
|
|
if app_cls:
|
|
app = msg(app_cls, "sharedApplication")
|
|
if app:
|
|
NSApplicationActivationPolicyRegular = 0
|
|
msg(app, "setActivationPolicy:", NSApplicationActivationPolicyRegular, argtypes=[ctypes.c_long])
|
|
|
|
# Set Dock Icon via ctypes since PyObjC might be missing
|
|
try:
|
|
from knoe import config as inst_config
|
|
cfg_icon = inst_config.get_ui_icon_image_path()
|
|
if cfg_icon.exists():
|
|
ns_image_cls = objc.objc_getClass(b"NSImage")
|
|
if ns_image_cls:
|
|
icon_path_ns = to_ns(str(cfg_icon))
|
|
img = msg(ns_image_cls, "alloc")
|
|
img = msg(img, "initWithContentsOfFile:", icon_path_ns, argtypes=[ctypes.c_void_p])
|
|
if img:
|
|
msg(app, "setApplicationIconImage:", img, argtypes=[ctypes.c_void_p])
|
|
except Exception:
|
|
pass
|
|
|
|
# 3. Update Bundle info
|
|
bundle_cls = objc.objc_getClass(b"NSBundle")
|
|
main_bundle = msg(bundle_cls, "mainBundle")
|
|
if main_bundle:
|
|
info_dict = msg(main_bundle, "localizedInfoDictionary")
|
|
if not info_dict:
|
|
info_dict = msg(main_bundle, "infoDictionary")
|
|
|
|
if info_dict:
|
|
# Some dicts might not support setObject:forKey:
|
|
set_obj_sel = objc.sel_registerName(b"setObject:forKey:")
|
|
if msg(info_dict, "respondsToSelector:", set_obj_sel, argtypes=[ctypes.c_void_p], restype=ctypes.c_bool):
|
|
msg(
|
|
info_dict,
|
|
"setObject:forKey:",
|
|
app_name_ns,
|
|
to_ns("CFBundleName"),
|
|
argtypes=[ctypes.c_void_p, ctypes.c_void_p],
|
|
)
|
|
msg(
|
|
info_dict,
|
|
"setObject:forKey:",
|
|
app_name_ns,
|
|
to_ns("CFBundleDisplayName"),
|
|
argtypes=[ctypes.c_void_p, ctypes.c_void_p],
|
|
)
|
|
|
|
# 3. Update Menu items
|
|
app_cls = objc.objc_getClass(b"NSApplication")
|
|
if app_cls:
|
|
app = msg(app_cls, "sharedApplication")
|
|
if app:
|
|
# IMPORTANT: Only modify menus if they are already initialized
|
|
main_menu = msg(app, "mainMenu")
|
|
if main_menu:
|
|
num_items = msg(main_menu, "numberOfItems", restype=ctypes.c_long)
|
|
if num_items > 0:
|
|
apple_menu_item = msg(
|
|
main_menu, "itemAtIndex:", 0, argtypes=[ctypes.c_long]
|
|
)
|
|
if apple_menu_item:
|
|
msg(
|
|
apple_menu_item,
|
|
"setTitle:",
|
|
app_name_ns,
|
|
argtypes=[ctypes.c_void_p],
|
|
)
|
|
apple_menu = msg(apple_menu_item, "submenu")
|
|
if apple_menu:
|
|
# About item
|
|
about_item = msg(
|
|
apple_menu, "itemAtIndex:", 0, argtypes=[ctypes.c_long]
|
|
)
|
|
if about_item:
|
|
msg(
|
|
about_item,
|
|
"setTitle:",
|
|
to_ns(f"About {app_name_str}"),
|
|
argtypes=[ctypes.c_void_p],
|
|
)
|
|
|
|
# Quit item - try by title first
|
|
quit_item = msg(
|
|
apple_menu,
|
|
"itemWithTitle:",
|
|
to_ns("Quit Python"),
|
|
argtypes=[ctypes.c_void_p],
|
|
)
|
|
if not quit_item:
|
|
# Search for terminate: action
|
|
num_menu_items = msg(
|
|
apple_menu, "numberOfItems", restype=ctypes.c_long
|
|
)
|
|
terminate_sel = objc.sel_registerName(b"terminate:")
|
|
for i in range(num_menu_items):
|
|
item = msg(
|
|
apple_menu,
|
|
"itemAtIndex:",
|
|
i,
|
|
argtypes=[ctypes.c_long],
|
|
)
|
|
if msg(item, "action") == terminate_sel:
|
|
quit_item = item
|
|
break
|
|
|
|
if quit_item:
|
|
msg(
|
|
quit_item,
|
|
"setTitle:",
|
|
to_ns(f"Quit {app_name_str}"),
|
|
argtypes=[ctypes.c_void_p],
|
|
)
|
|
except Exception:
|
|
pass
|
|
|
|
|
|
from knoe.core.env import * # noqa: F401,F403
|
|
from knoe.core.actions import (
|
|
KnoeInstaller,
|
|
KnoeConsoleInstaller,
|
|
_prepare_k3s_pipeline,
|
|
)
|
|
|
|
# Keep global frames list in memory (legacy requirement).
|
|
GLOBAL_SCAN_FRAMES: list = []
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Mixin imports — one per screen / concern
|
|
# ---------------------------------------------------------------------------
|
|
from knoe.ui.screens.base import ScreenBaseMixin
|
|
from knoe.ui.screens.navigation import NavigationMixin
|
|
from knoe.ui.screens.welcome import WelcomeScreenMixin
|
|
from knoe.ui.screens.dependencies import DependenciesScreenMixin
|
|
from knoe.ui.screens.network import NetworkScreenMixin
|
|
from knoe.ui.screens.environment import EnvironmentScreenMixin
|
|
from knoe.ui.screens.database import DatabaseScreenMixin
|
|
from knoe.ui.screens.database_options import DatabaseOptionsScreenMixin
|
|
from knoe.ui.screens.cluster import ClusterScreenMixin
|
|
from knoe.ui.screens.cluster_nodes import ClusterNodesScreenMixin
|
|
from knoe.ui.screens.services import ServicesScreenMixin
|
|
from knoe.ui.screens.security import SecurityScreenMixin
|
|
from knoe.ui.screens.knoe_users import KnoeUsersScreenMixin
|
|
from knoe.ui.screens.supabase import SupabaseScreenMixin
|
|
from knoe.ui.screens.gitops import GitOpsScreenMixin
|
|
from knoe.ui.screens.argocd import ArgoCDScreenMixin
|
|
from knoe.ui.screens.docker import DockerScreenMixin
|
|
from knoe.ui.screens.build import BuildScreenMixin
|
|
from knoe.ui.screens.packaging import PackagingScreenMixin
|
|
from knoe.ui.screens.deploy import DeployScreenMixin
|
|
from knoe.ui.screens.validate import ValidateScreenMixin
|
|
from knoe.ui.screens.cfg import ConfigMixin
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Composed installer class
|
|
# ---------------------------------------------------------------------------
|
|
class KnoeInstaller(
|
|
KnoeInstaller,
|
|
ScreenBaseMixin,
|
|
NavigationMixin,
|
|
WelcomeScreenMixin,
|
|
DependenciesScreenMixin,
|
|
NetworkScreenMixin,
|
|
EnvironmentScreenMixin,
|
|
DatabaseScreenMixin,
|
|
DatabaseOptionsScreenMixin,
|
|
ClusterScreenMixin,
|
|
ClusterNodesScreenMixin,
|
|
ServicesScreenMixin,
|
|
SecurityScreenMixin,
|
|
KnoeUsersScreenMixin,
|
|
SupabaseScreenMixin,
|
|
GitOpsScreenMixin,
|
|
ArgoCDScreenMixin,
|
|
DockerScreenMixin,
|
|
BuildScreenMixin,
|
|
PackagingScreenMixin,
|
|
DeployScreenMixin,
|
|
ValidateScreenMixin,
|
|
ConfigMixin,
|
|
):
|
|
"""Knoe GUI installer — assembled from per-screen mixins."""
|
|
|
|
KUBECTL_STATUS_TAB = "__kubectl_status__"
|
|
|
|
# ---- Data access bridge (satisfies KnoeInstallerBase contract) ----
|
|
_INPUT_VAR_MAP: dict[str, str] = {
|
|
"init_cluster.cluster_env": "cluster_env",
|
|
"init_password.db_namespace": "db_namespace",
|
|
"init_password.cluster_name": "cnpg_cluster_name",
|
|
"init_password.db_username": "db_username",
|
|
"init_password.db_password": "db_password",
|
|
"init_password.db_host_port": "db_host_port",
|
|
"env_setup.DATABASE_NAMESPACE": "db_namespace",
|
|
"env_setup.CLUSTER_NAME": "cnpg_cluster_name",
|
|
"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.argocd_enabled": "argocd_enabled",
|
|
"init_cluster.at_rest_encryption_enabled": "at_rest_encryption_enabled",
|
|
"init_cluster.selected_kubectx": "selected_kubectx",
|
|
"init_cluster.app_cluster_kubecontext": "app_cluster_kubecontext",
|
|
"init_cluster.db_cluster_kubecontext": "db_cluster_kubecontext",
|
|
"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",
|
|
"argocd.namespace": "argocd_namespace",
|
|
}
|
|
|
|
def _get_input(self, key: str, default: str | None = None) -> str:
|
|
"""Bridge for :class:`KnoeInstallerBase` — 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:
|
|
conf_dir = knoe_conf.resolve_knoe_conf_dir(PROJECT_ROOT)
|
|
resolved_cfg = knoe_conf.entrypoint_path(conf_dir)
|
|
|
|
self.controller = KnoeController(
|
|
PROJECT_ROOT, verbose=verbose, cfg_path=resolved_cfg
|
|
)
|
|
self.project_root = PROJECT_ROOT
|
|
self.cfg_path = resolved_cfg
|
|
from knoe.core.env import DEFAULT_ACTION_FLAGS
|
|
self._action_flags: dict[str, bool] = DEFAULT_ACTION_FLAGS.copy()
|
|
self._init_shared_state()
|
|
self._init_database_options_state()
|
|
self.screens = None
|
|
self.deploy_environment = None
|
|
self.root = root
|
|
self.root.title("Knoe.DB Installer")
|
|
try:
|
|
setup_macos_app_identity()
|
|
except Exception:
|
|
pass
|
|
self._cfg_path_override = None
|
|
|
|
# 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/knoeLogoSepia.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"),
|
|
("Cluster Nodes", "cluster_nodes"),
|
|
("Common Services", "common_services"),
|
|
("Database Options", "database_options"),
|
|
("Docker Build", "init_db_build"),
|
|
("Database Creation", "init_password"),
|
|
("Initialization Scripts", "init_scripts"),
|
|
("Knoe Authority", "kerberos_config"),
|
|
("Knoe Users", "knoe_users"),
|
|
("ArgoCD", "argocd_config"),
|
|
("GitOps", "gitops_config"),
|
|
("Supabase", "supabase_config"),
|
|
("Deployment", "init_cnpg_deploy"),
|
|
("Security", "security"),
|
|
]
|
|
self.nav_widgets = {}
|
|
self._mode_tab_widgets: dict = {}
|
|
self.deployment_mode = tk.StringVar(value=__import__("os").environ.get("KNOE_MODE", ""))
|
|
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 knoe.cfg data (override base with tk-aware version)
|
|
self.knoe_cfg_data = {
|
|
"Global": {},
|
|
"Welcome": {},
|
|
"Dependencies": {},
|
|
"Network": {},
|
|
"Port Forwards": {},
|
|
"System Environment": {},
|
|
"Monitoring": {},
|
|
"Kerberos Authentication": {},
|
|
"Knoe User Accounts": {},
|
|
"Ollama": {},
|
|
"Optional Features": {},
|
|
"Database Creation": {},
|
|
"Docker Build": {},
|
|
"Initialize Cluster": {},
|
|
"Cluster Nodes": {},
|
|
"Initialization Scripts": {},
|
|
"GitOps": {},
|
|
"Deployment": {},
|
|
"Dev Cluster (k3d)": {},
|
|
"Service Cluster (k3s)": {},
|
|
"Prod Cluster (k8s)": {},
|
|
"Install": {},
|
|
}
|
|
self._cfg_secret_cache = {}
|
|
self._secrets_finalized = False
|
|
# Must exist before reading knoe.cfg so saved values are applied (avoid silent fallbacks).
|
|
self.cluster_env = tk.StringVar(value="dev")
|
|
self.service_namespace = tk.StringVar(value="default")
|
|
self.gitops_enabled = tk.BooleanVar(value=False)
|
|
self.gitops_namespace = tk.StringVar(value="gitea")
|
|
self.gitops_node_selector = tk.StringVar(value="")
|
|
self.gitops_git_provider = tk.StringVar(value="Gitea")
|
|
self.argocd_enabled = tk.BooleanVar(value=False)
|
|
self.argocd_namespace = tk.StringVar(value="argocd")
|
|
self.argocd_node_selector = tk.StringVar(value="")
|
|
|
|
# Load existing docker_import_dir from knoe.cfg
|
|
try:
|
|
conf_dir = self._resolve_knoe_conf_dir()
|
|
try:
|
|
knoe_conf.ensure_entrypoint(conf_dir)
|
|
except Exception:
|
|
pass
|
|
cfg_path = knoe_conf.entrypoint_path(conf_dir)
|
|
if self._cfg_path_override:
|
|
cfg_path = self._cfg_path_override
|
|
if cfg_path.exists():
|
|
cfg = knoe_conf.load_layered_config(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.knoe_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("DATABASE_NAMESPACE", ""), cfg_vars
|
|
).strip()
|
|
if not saved_ns:
|
|
saved_ns = _expand_cfg_value(
|
|
cfg["Global"].get("NAMESPACE", ""), cfg_vars
|
|
).strip()
|
|
saved_cluster_name = _expand_cfg_value(
|
|
cfg["Global"].get("CLUSTER_NAME", ""), cfg_vars
|
|
).strip()
|
|
if not saved_cluster_name:
|
|
saved_cluster_name = _expand_cfg_value(
|
|
cfg["Global"].get("CNPG_CLUSTER_NAME", ""), cfg_vars
|
|
).strip()
|
|
saved_service_ns = _expand_cfg_value(
|
|
cfg["Global"].get("SERVICE_NAMESPACE", ""), cfg_vars
|
|
).strip()
|
|
saved_argocd_ns = _expand_cfg_value(
|
|
cfg["Global"].get("ARGOCD_NAMESPACE", ""), cfg_vars
|
|
).strip()
|
|
saved_gitops_node = _expand_cfg_value(
|
|
cfg["Global"].get("GITEA_NODE_SELECTOR", ""), cfg_vars
|
|
).strip()
|
|
saved_argocd_node = _expand_cfg_value(
|
|
cfg["Global"].get("ARGOCD_NODE_SELECTOR", ""), 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_argocd_enabled = (
|
|
_expand_cfg_value(
|
|
cfg.get(
|
|
"Optional Features",
|
|
"ARGOCD_ENABLED",
|
|
fallback="",
|
|
),
|
|
cfg_vars,
|
|
).strip()
|
|
if cfg.has_section("Optional Features")
|
|
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
|
|
try:
|
|
self.knoe_cfg_data.setdefault("Global", {})[
|
|
"CLUSTER_ENV"
|
|
] = saved_cluster
|
|
except Exception:
|
|
pass
|
|
if saved_ns:
|
|
try:
|
|
self.db_namespace.set(saved_ns)
|
|
except Exception:
|
|
pass
|
|
try:
|
|
self.knoe_cfg_data.setdefault("Global", {})[
|
|
"DATABASE_NAMESPACE"
|
|
] = saved_ns
|
|
except Exception:
|
|
pass
|
|
if saved_cluster_name:
|
|
try:
|
|
self.cnpg_cluster_name.set(saved_cluster_name)
|
|
except Exception:
|
|
pass
|
|
try:
|
|
self.knoe_cfg_data.setdefault("Global", {})[
|
|
"CLUSTER_NAME"
|
|
] = saved_cluster_name
|
|
except Exception:
|
|
pass
|
|
if not saved_service_ns or saved_service_ns == "default":
|
|
# Seed knoe-system for all managed environments so the
|
|
# UI never shows the bare "default" namespace.
|
|
_env_hint = knoe_conf._env_from_mode_or_env_hint(
|
|
saved_cluster or os.environ.get("CLUSTER_ENV", "")
|
|
)
|
|
if _env_hint in ("dev", "service", "prod"):
|
|
saved_service_ns = "knoe-system"
|
|
if saved_service_ns:
|
|
try:
|
|
self.service_namespace.set(saved_service_ns)
|
|
except Exception:
|
|
pass
|
|
self.knoe_cfg_data["Global"][
|
|
"SERVICE_NAMESPACE"
|
|
] = saved_service_ns
|
|
if saved_argocd_ns:
|
|
try:
|
|
self.argocd_namespace.set(saved_argocd_ns)
|
|
except Exception:
|
|
pass
|
|
self.knoe_cfg_data.setdefault("Global", {})[
|
|
"ARGOCD_NAMESPACE"
|
|
] = saved_argocd_ns
|
|
if saved_gitops_ns:
|
|
try:
|
|
self.gitops_namespace.set(saved_gitops_ns)
|
|
except Exception:
|
|
pass
|
|
self.knoe_cfg_data.setdefault("GitOps", {})[
|
|
"NAMESPACE"
|
|
] = saved_gitops_ns
|
|
if saved_gitops_node:
|
|
try:
|
|
self.gitops_node_selector.set(saved_gitops_node)
|
|
except Exception:
|
|
pass
|
|
self.knoe_cfg_data.setdefault("Global", {})[
|
|
"GITEA_NODE_SELECTOR"
|
|
] = saved_gitops_node
|
|
if saved_argocd_node:
|
|
try:
|
|
self.argocd_node_selector.set(saved_argocd_node)
|
|
except Exception:
|
|
pass
|
|
self.knoe_cfg_data.setdefault("Global", {})[
|
|
"ARGOCD_NODE_SELECTOR"
|
|
] = saved_argocd_node
|
|
if saved_argocd_enabled:
|
|
val = saved_argocd_enabled.lower() in ("true", "1", "yes")
|
|
try:
|
|
self.argocd_enabled.set(val)
|
|
except Exception:
|
|
pass
|
|
self.knoe_cfg_data.setdefault("Optional Features", {})[
|
|
"ARGOCD_ENABLED"
|
|
] = str(val)
|
|
if saved_gitops_enabled:
|
|
val = saved_gitops_enabled.lower() in ("true", "1", "yes")
|
|
try:
|
|
self.gitops_enabled.set(val)
|
|
except Exception:
|
|
pass
|
|
self.knoe_cfg_data.setdefault("Optional Features", {})[
|
|
"GITOPS_ENABLED"
|
|
] = str(val)
|
|
|
|
if cfg.has_section("Monitoring"):
|
|
mon = {}
|
|
for k, v in cfg.items("Monitoring"):
|
|
try:
|
|
mon[k] = _expand_cfg_value(v, cfg_vars)
|
|
except Exception:
|
|
mon[k] = v
|
|
if mon:
|
|
self.knoe_cfg_data["Monitoring"] = mon
|
|
|
|
if cfg.has_section("Prod Cluster (k8s)"):
|
|
prod_sec = {}
|
|
for k, v in cfg.items("Prod Cluster (k8s)"):
|
|
try:
|
|
prod_sec[k.upper()] = _expand_cfg_value(v, cfg_vars)
|
|
except Exception:
|
|
prod_sec[k.upper()] = v
|
|
if prod_sec:
|
|
self.knoe_cfg_data["Prod Cluster (k8s)"].update(prod_sec)
|
|
|
|
gcp_cfg_path = conf_dir / "prod" / "gcp.cfg"
|
|
gcp_values = _parse_gcp_cfg(gcp_cfg_path)
|
|
if gcp_values:
|
|
self.knoe_cfg_data.setdefault("GCP", {}).update(
|
|
{k.upper(): v for k, v in gcp_values.items()}
|
|
)
|
|
|
|
# Preserve raw shell/config expressions for persistence so paths
|
|
# like `${HOME}/...` remain portable across hosts.
|
|
for section in cfg.sections():
|
|
section_data = self.knoe_cfg_data.setdefault(section, {})
|
|
for key, raw_value in cfg.items(section):
|
|
section_data[key] = _preserve_cfg_expr_for_persistence(
|
|
raw_value,
|
|
section_data.get(key, raw_value),
|
|
)
|
|
except Exception:
|
|
pass
|
|
|
|
# NOTE: Monitoring storage base paths are currently pinned in the
|
|
# monitoring deploy script/Helm values (not persisted in knoe.cfg).
|
|
self.knoe_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
|
|
# NOTE: cluster_env is initialized before config load; keep it stable here.
|
|
try:
|
|
if not (self.cluster_env.get() or "").strip():
|
|
self.cluster_env.set("dev")
|
|
except Exception:
|
|
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.knoe_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.app_cluster_kubecontext = tk.StringVar()
|
|
self.db_cluster_kubecontext = tk.StringVar()
|
|
try:
|
|
# Prefer explicit knoe.cfg selection; fall back to the dev-cluster section.
|
|
saved_ctx = (
|
|
(self.knoe_cfg_data.get("Global", {}) or {}).get("KUBECONTEXT", "")
|
|
or (self.knoe_cfg_data.get("Dev Cluster (k3d)", {}) or {}).get(
|
|
"KUBECTL_CONTEXT", ""
|
|
)
|
|
)
|
|
saved_ctx = (saved_ctx or "").strip()
|
|
if saved_ctx:
|
|
self.selected_kubectx.set(saved_ctx)
|
|
# Pre-populate dual App/DB context selectors from [Inputs]
|
|
inputs = self.knoe_cfg_data.get("Inputs", {}) or {}
|
|
saved_app = str(inputs.get("init_cluster.app_cluster_kubecontext") or "").strip()
|
|
saved_db = str(inputs.get("init_cluster.db_cluster_kubecontext") or "").strip()
|
|
if saved_app:
|
|
self.app_cluster_kubecontext.set(saved_app)
|
|
if saved_db:
|
|
self.db_cluster_kubecontext.set(saved_db)
|
|
except Exception:
|
|
pass
|
|
self.prod_artifacts_path = tk.StringVar(
|
|
value=str(PROJECT_ROOT / "data" / "staging")
|
|
)
|
|
saved_artifacts_dir = (
|
|
self.knoe_cfg_data.get("Prod Cluster (k8s)", {})
|
|
.get("ARTIFACTS_DIR", "")
|
|
.strip()
|
|
)
|
|
if saved_artifacts_dir:
|
|
try:
|
|
self.prod_artifacts_path.set(saved_artifacts_dir)
|
|
except Exception:
|
|
pass
|
|
# Stash GCP values so _ensure_prod_config_state can apply them lazily.
|
|
self._gcp_cfg_values = dict(self.knoe_cfg_data.get("GCP", {}))
|
|
# GitOps provider choice (gitea / argocd / none)
|
|
self.gitops_provider = tk.StringVar(value="gitea")
|
|
# Google Workspace
|
|
self.gworkspace_domain = tk.StringVar()
|
|
self.gworkspace_customer_id = tk.StringVar()
|
|
self.gworkspace_admin_email = tk.StringVar()
|
|
# Google Cloud
|
|
self.gcloud_project_id = tk.StringVar()
|
|
self.gcloud_region = tk.StringVar(value="us-central1")
|
|
self.gcloud_sa_key_path = tk.StringVar()
|
|
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="knoe")
|
|
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_db_namespace())
|
|
self.cnpg_cluster_name = tk.StringVar(value=self._cnpg_cluster_name())
|
|
try:
|
|
_ns_from_cfg = (
|
|
(self.knoe_cfg_data.get("Global", {}) or {})
|
|
.get("DATABASE_NAMESPACE", "")
|
|
.strip()
|
|
)
|
|
if not _ns_from_cfg:
|
|
_ns_from_cfg = (
|
|
(self.knoe_cfg_data.get("Global", {}) or {})
|
|
.get("NAMESPACE", "")
|
|
.strip()
|
|
)
|
|
if _ns_from_cfg:
|
|
base = self._ensure_namespace_prefix(_ns_from_cfg)
|
|
ver = ""
|
|
try:
|
|
ver = (self.controller.get_knoe_db_version() or "").strip()
|
|
except Exception:
|
|
ver = ""
|
|
if ver:
|
|
ver_ns = self._sanitize_namespace(ver.replace(".", "-"))
|
|
base_ns = self._sanitize_namespace(base)
|
|
if base_ns.endswith(f"-{ver_ns}"):
|
|
self.db_namespace.set(base_ns)
|
|
else:
|
|
self.db_namespace.set(self._sanitize_namespace(f"{base_ns}-{ver_ns}"))
|
|
else:
|
|
self.db_namespace.set(self._sanitize_namespace(base))
|
|
except Exception:
|
|
pass
|
|
self.db_host_port = tk.StringVar(value="5432")
|
|
self._last_synced_ns = self.db_namespace.get()
|
|
self.db_namespace.trace_add("write", self._propagate_namespace_change)
|
|
|
|
# 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)
|
|
_global_cfg = self.knoe_cfg_data.get("Global", {}) or {}
|
|
_supabase_pv_node = (
|
|
(_global_cfg.get("SUPABASE_PV_NODE", "") or "").strip()
|
|
or (os.environ.get("SUPABASE_PV_NODE") or "").strip()
|
|
)
|
|
_supabase_pv_base = (
|
|
(_global_cfg.get("SUPABASE_PV_BASE_DIR", "") or "").strip()
|
|
or (_global_cfg.get("SUPABASE_PV_BASE", "") or "").strip()
|
|
or (os.environ.get("SUPABASE_PV_BASE_DIR") or "").strip()
|
|
or (os.environ.get("SUPABASE_PV_BASE") or "").strip()
|
|
or "/synology/d005"
|
|
)
|
|
self.supabase_pv_node = tk.StringVar(value=_supabase_pv_node)
|
|
self.supabase_pv_base_dir = tk.StringVar(value=_supabase_pv_base)
|
|
self.at_rest_encryption_enabled = tk.BooleanVar(value=True)
|
|
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
|
|
# 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(
|
|
"knoe_users", self._render_knoe_users_page
|
|
)
|
|
self._register_canvas_renderer(
|
|
"supabase_config", self._render_supabase_config_page
|
|
)
|
|
self._register_canvas_renderer("argocd_config", self._render_argocd_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("cluster_nodes", self._render_cluster_nodes_page)
|
|
self._register_canvas_renderer(
|
|
"common_services", self._render_common_services_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("security", self._render_security_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("cluster_nodes", None)
|
|
self._register_page("common_services", None)
|
|
self._register_page("database_options", None)
|
|
self._register_page("init_db_build", None)
|
|
self._register_page("init_password", None)
|
|
self._register_page("init_scripts", None)
|
|
self._register_page("kerberos_config", None)
|
|
self._register_page("knoe_users", None)
|
|
self._register_page("argocd_config", None)
|
|
self._register_page("gitops_config", None)
|
|
self._register_page("supabase_config", None)
|
|
self._register_page("init_cnpg_deploy", None)
|
|
self._register_page("security", 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_knoe_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="Knoe.DB Installer")
|
|
parser.add_argument(
|
|
"-c",
|
|
"--config",
|
|
default=None,
|
|
help="Path to knoe.cfg for saving or silent replay",
|
|
)
|
|
parser.add_argument(
|
|
"-s",
|
|
"-S",
|
|
"--silent",
|
|
action="store_true",
|
|
help="Run unattended install in console mode using knoe.cfg",
|
|
)
|
|
parser.add_argument(
|
|
"-l",
|
|
"--log",
|
|
nargs="?",
|
|
const=True,
|
|
default=None,
|
|
help="Enable logging to file. Optionally provide a path (default: /tmp/knoe_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(
|
|
"--delete-db",
|
|
action="store_true",
|
|
default=False,
|
|
help=argparse.SUPPRESS,
|
|
)
|
|
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(
|
|
"--repair",
|
|
action="store_true",
|
|
help="Run unattended repair flow using knoe.cfg",
|
|
)
|
|
parser.add_argument(
|
|
"--update",
|
|
action="store_true",
|
|
help="Run unattended update flow using knoe.cfg",
|
|
)
|
|
parser.add_argument(
|
|
"--min",
|
|
action="store_true",
|
|
help="Run absolute minimal installation (containerd based, no Kubernetes)",
|
|
)
|
|
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()
|
|
|
|
if args.delete_db and not args.reset:
|
|
parser.error("--delete-db requires --reset")
|
|
|
|
# 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/knoe_install_output.log"
|
|
else:
|
|
log_file_path = args.log
|
|
|
|
from knoe.config import setup_logging
|
|
|
|
setup_logging(verbose=args.verbose, debug=args.debug)
|
|
|
|
controller = KnoeController(
|
|
PROJECT_ROOT,
|
|
verbose=args.verbose,
|
|
cfg_path=Path(args.config).expanduser() if args.config else None,
|
|
)
|
|
|
|
if getattr(args, "min", False):
|
|
controller.state.inputs["init_cluster.cluster_env"] = "min"
|
|
# Enable Kerberos by default in min mode
|
|
controller.state.inputs["kerberos_config.enabled"] = "True"
|
|
# Skip heavier workloads
|
|
controller.state.inputs["init_cluster.supabase_enabled"] = "False"
|
|
controller.state.inputs["gitops.enabled"] = "False"
|
|
controller.state.inputs["argocd.enabled"] = "False"
|
|
|
|
if args.command == "monitor":
|
|
from knoe.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.repair and args.update:
|
|
parser.error("--repair and --update cannot be used together")
|
|
|
|
if args.repair or args.update:
|
|
args.silent = True
|
|
|
|
if args.silent:
|
|
ensure_op_signed_in()
|
|
ensure_knoey_vault()
|
|
installer = KnoeConsoleInstaller(
|
|
controller,
|
|
args.config,
|
|
reset_cluster=args.reset,
|
|
delete_db=args.delete_db,
|
|
)
|
|
if log_file_path:
|
|
installer.enable_log_file(log_file_path)
|
|
if args.update:
|
|
rc = installer.run_update()
|
|
elif args.repair:
|
|
rc = installer.run_repair()
|
|
else:
|
|
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:
|
|
# Set macOS identity as early as possible
|
|
try:
|
|
setup_macos_app_identity()
|
|
except Exception:
|
|
pass
|
|
root = tk.Tk()
|
|
root.title("Knoe.DB Installer")
|
|
# Set macOS identity again AFTER tk.Tk() but BEFORE mainloop
|
|
try:
|
|
setup_macos_app_identity()
|
|
except Exception:
|
|
pass
|
|
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_op_signed_in()
|
|
ensure_knoey_vault()
|
|
KnoeInstaller(
|
|
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 knoe.ncurses_installer import run_ncurses_installer
|
|
|
|
run_ncurses_installer(controller)
|
|
else:
|
|
ensure_op_signed_in()
|
|
ensure_knoey_vault()
|
|
from knoe.ncurses_installer import run_ncurses_installer
|
|
|
|
run_ncurses_installer(controller)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|