mirror of
https://github.com/dredx/prole.git
synced 2026-09-23 11:03:59 +00:00
2128 lines
83 KiB
Python
2128 lines
83 KiB
Python
"""Initialization scripts, CNPG deployment and service-layer overlays."""
|
|
|
|
import os
|
|
import shlex
|
|
import subprocess
|
|
import threading
|
|
import time
|
|
import tkinter as tk
|
|
import webbrowser
|
|
from datetime import datetime
|
|
from pathlib import Path
|
|
from tkinter import ttk, messagebox, filedialog
|
|
|
|
import platform
|
|
from knoe import screen as ui
|
|
from knoe.core.env import (
|
|
PROJECT_ROOT,
|
|
_bool_str,
|
|
_deployment_mode_from_env,
|
|
_deployment_target_label,
|
|
_normalize_cluster_env,
|
|
)
|
|
from knoe.core.ops import garage_store as garage_store_ops
|
|
from knoe.core.ops import monitoring as monitoring_ops
|
|
from knoe.core.ops import openbao as openbao_ops
|
|
from knoe.core.ops import opentofu as opentofu_ops
|
|
from knoe.core.ops import registry as registry_ops
|
|
from knoe.core.ops.cloudnative_pg import (
|
|
initialize as cnpg_initialize,
|
|
deploy as cnpg_deploy,
|
|
rollout as cnpg_rollout,
|
|
)
|
|
from knoe.core.policy import (
|
|
POLICY_CFG_KEY,
|
|
OPTIONAL_WORKLOADS_MIN_READY_SCHEDULABLE_NODES,
|
|
evaluate_optional_workloads_allowed,
|
|
)
|
|
|
|
|
|
class ServicesScreenMixin:
|
|
"""Initialization scripts, CNPG deployment and service-layer overlays."""
|
|
|
|
# Traffic light color constants
|
|
_TL_YELLOW = "#ff9f0a"
|
|
_TL_GREEN = "#34c759"
|
|
_TL_RED = "#ff3b30"
|
|
|
|
def _set_init_light(self, key: str, state: str) -> None:
|
|
"""Update a traffic light on the Initialization Scripts page.
|
|
|
|
state: 'yellow' | 'green' | 'red'
|
|
"""
|
|
color = {
|
|
"yellow": self._TL_YELLOW,
|
|
"green": self._TL_GREEN,
|
|
"red": self._TL_RED,
|
|
}.get(state, self._TL_YELLOW)
|
|
light = getattr(self, "_init_traffic_lights", {}).get(key)
|
|
if light is None:
|
|
return
|
|
try:
|
|
self.bg_canvas.itemconfig(light, fill=color)
|
|
except Exception:
|
|
pass
|
|
|
|
def _build_gke_registry_env(self) -> dict[str, str]:
|
|
"""Resolve GKE Artifact Registry env values from persisted config.
|
|
|
|
Returns a dict with any available keys among:
|
|
- ``GCP_PROJECT_ID``
|
|
- ``GCP_REGION``
|
|
- ``ARTIFACT_REGISTRY``
|
|
"""
|
|
resolved: dict[str, str] = {}
|
|
try:
|
|
self._load_gcp_cfg_into_knoe_data()
|
|
except Exception:
|
|
pass
|
|
|
|
gcp = ((getattr(self, "knoe_cfg_data", {}) or {}).get("GCP", {}) or {})
|
|
global_cfg = ((getattr(self, "knoe_cfg_data", {}) or {}).get("Global", {}) or {})
|
|
|
|
project_id = (
|
|
(gcp.get("project_id") or gcp.get("PROJECT_ID") or "")
|
|
.strip()
|
|
.strip('"')
|
|
)
|
|
if project_id:
|
|
resolved["GCP_PROJECT_ID"] = project_id
|
|
|
|
region = (
|
|
(gcp.get("region") or gcp.get("REGION") or gcp.get("location") or "")
|
|
.strip()
|
|
.strip('"')
|
|
)
|
|
if not region:
|
|
kubecontext = (global_cfg.get("KUBECONTEXT") or "").strip()
|
|
if kubecontext.startswith("gke_"):
|
|
parts = kubecontext.split("_", 3)
|
|
if len(parts) >= 3:
|
|
region = (parts[2] or "").strip()
|
|
if region:
|
|
resolved["GCP_REGION"] = region
|
|
|
|
artifact_registry = (global_cfg.get("ARTIFACT_REGISTRY") or "").strip().strip('"')
|
|
if not artifact_registry and project_id and region:
|
|
repo = (global_cfg.get("SERVICE_NAMESPACE") or "knoe-system").strip() or "knoe-system"
|
|
artifact_registry = f"{region}-docker.pkg.dev/{project_id}/{repo}"
|
|
if artifact_registry:
|
|
resolved["ARTIFACT_REGISTRY"] = artifact_registry
|
|
|
|
return resolved
|
|
|
|
def _render_init_scripts_page(self):
|
|
content_width = self.bg_canvas.winfo_width() or 975
|
|
right_margin = content_width - 48
|
|
app_cluster = (
|
|
self._get_input("init_password.app_cluster_name", "")
|
|
or self._get_input("env_setup.APP_CLUSTER_NAME", "")
|
|
or "knoe-dev-0"
|
|
).strip() or "knoe-dev-0"
|
|
db_cluster = (
|
|
self._get_input("init_password.db_cluster_name", "")
|
|
or self._get_input("env_setup.DB_CLUSTER_NAME", "")
|
|
or "knoe-cnpg-0"
|
|
).strip() or "knoe-cnpg-0"
|
|
|
|
ui.canvas_text(
|
|
self, right_margin, 40, "knoe.dev",
|
|
fill="#6e6e73", font=("SF Pro Text", 32, "bold"), anchor="ne",
|
|
)
|
|
ui.canvas_text(
|
|
self, right_margin, 85, "infrastructure.auto()",
|
|
fill="#6e6e73", font=("SF Pro Text", 18), anchor="ne",
|
|
)
|
|
self._render_title("Initialization Scripts", y=150)
|
|
|
|
ui.canvas_text(
|
|
self,
|
|
48,
|
|
178,
|
|
f"Application Cluster: {app_cluster} (Autopilot)",
|
|
fill="#1d1d1f",
|
|
font=("SF Pro Text", 11),
|
|
anchor="nw",
|
|
)
|
|
ui.canvas_text(
|
|
self,
|
|
48,
|
|
196,
|
|
f"Database Cluster: {db_cluster} (Standard)",
|
|
fill="#1d1d1f",
|
|
font=("SF Pro Text", 11),
|
|
anchor="nw",
|
|
)
|
|
ui.canvas_text(
|
|
self,
|
|
48,
|
|
214,
|
|
"CloudNativePG runs on the dedicated Standard GKE cluster; platform services remain on the Autopilot cluster.",
|
|
fill="#6e6e73",
|
|
font=("SF Pro Text", 10),
|
|
anchor="nw",
|
|
)
|
|
|
|
x_label = 48
|
|
gy = 242 # top of traffic-light section
|
|
|
|
# --- Traffic light section ---
|
|
# Column x positions
|
|
col_common = x_label
|
|
col_db = 330
|
|
col_plat = 645
|
|
|
|
# Group headers
|
|
for col_x, header in (
|
|
(col_common, "Common Services"),
|
|
(col_db, "Database Services"),
|
|
(col_plat, "Platform Services"),
|
|
):
|
|
ui.canvas_text(
|
|
self, col_x, gy, header,
|
|
fill="#1d1d1f", font=("SF Pro Text", 11, "bold"), anchor="nw",
|
|
)
|
|
|
|
# Common Services — one indicator
|
|
lights: dict[str, int] = {}
|
|
ly = gy + 24
|
|
indicator = self.bg_canvas.create_oval(
|
|
col_common, ly, col_common + 14, ly + 14,
|
|
fill=self._TL_YELLOW, outline="#8e8e93", width=1,
|
|
)
|
|
self._canvas_items.append(indicator)
|
|
ui.canvas_text(
|
|
self, col_common + 20, ly + 7, "Services Ready",
|
|
fill="black", font=("SF Pro Text", 10), anchor="w",
|
|
)
|
|
lights["common_services"] = indicator
|
|
|
|
# Database Services — knoe-db-1/2/3 + barman-cloud
|
|
db_items = [
|
|
("knoe_db_1", "knoe-db-1"),
|
|
("knoe_db_2", "knoe-db-2"),
|
|
("knoe_db_3", "knoe-db-3"),
|
|
("barman_cloud", "barman-cloud (backup)"),
|
|
]
|
|
for i, (key, label) in enumerate(db_items):
|
|
ly = gy + 24 + i * 24
|
|
ind = self.bg_canvas.create_oval(
|
|
col_db, ly, col_db + 14, ly + 14,
|
|
fill=self._TL_YELLOW, outline="#8e8e93", width=1,
|
|
)
|
|
self._canvas_items.append(ind)
|
|
ui.canvas_text(
|
|
self, col_db + 20, ly + 7, label,
|
|
fill="black", font=("SF Pro Text", 10), anchor="w",
|
|
)
|
|
lights[key] = ind
|
|
|
|
# Platform Services — monitoring + kong/ingress
|
|
plat_items = [
|
|
("monitoring", "Monitoring"),
|
|
("kong", "Kong / Ingress"),
|
|
]
|
|
for i, (key, label) in enumerate(plat_items):
|
|
ly = gy + 24 + i * 24
|
|
ind = self.bg_canvas.create_oval(
|
|
col_plat, ly, col_plat + 14, ly + 14,
|
|
fill=self._TL_YELLOW, outline="#8e8e93", width=1,
|
|
)
|
|
self._canvas_items.append(ind)
|
|
ui.canvas_text(
|
|
self, col_plat + 20, ly + 7, label,
|
|
fill="black", font=("SF Pro Text", 10), anchor="w",
|
|
)
|
|
lights[key] = ind
|
|
|
|
self._init_traffic_lights = lights
|
|
|
|
# Thin separator below lights
|
|
sep_y = gy + 125
|
|
self._canvas_items.append(
|
|
self.bg_canvas.create_line(
|
|
x_label, sep_y, right_margin, sep_y,
|
|
fill="#e0e0e0", width=1,
|
|
)
|
|
)
|
|
|
|
# --- Tabbed output section ---
|
|
nb_y = sep_y + 10
|
|
nb_height = 415
|
|
|
|
notebook_bg = tk.Frame(self.bg_canvas, bg="white", highlightthickness=0, bd=0)
|
|
self.script_tabs = ttk.Notebook(notebook_bg, style="TNotebook")
|
|
self.script_tabs.pack(fill="both", expand=True, padx=1, pady=1)
|
|
|
|
tab_win = self.bg_canvas.create_window(
|
|
x_label, nb_y, window=notebook_bg, anchor="nw",
|
|
width=right_margin - x_label, height=nb_height,
|
|
)
|
|
self._canvas_items.append(tab_win)
|
|
self._overlay_widgets.extend([notebook_bg, self.script_tabs])
|
|
|
|
self.script_consoles = {}
|
|
self._script_tab_index = {}
|
|
|
|
tab_defs = [
|
|
("Common Services", "tab_common"),
|
|
("Database Services", "tab_database"),
|
|
("Platform Services", "tab_platform"),
|
|
("Kubectl Status", self.KUBECTL_STATUS_TAB),
|
|
]
|
|
for idx, (tab_title, key) in enumerate(tab_defs):
|
|
console_bg = tk.Frame(self.script_tabs, bg="white", highlightthickness=0, bd=0)
|
|
self.script_tabs.add(console_bg, text=tab_title)
|
|
if key == self.KUBECTL_STATUS_TAB:
|
|
mono = ("Menlo", 9) if platform.system() == "Darwin" else ("Consolas", 9)
|
|
console = ui.TerminalConsole(
|
|
console_bg, highlightthickness=0, bd=0,
|
|
font=mono, wrap="none", show_horizontal=True,
|
|
)
|
|
else:
|
|
console = ui.TerminalConsole(console_bg, highlightthickness=0, bd=0)
|
|
console.pack(fill="both", expand=True, padx=1, pady=1)
|
|
self.script_consoles[key] = console
|
|
self._script_tab_index[key] = idx
|
|
self._overlay_widgets.extend([console_bg, console])
|
|
|
|
self.script_tabs.bind("<<NotebookTabChanged>>", self._on_init_scripts_tab_changed)
|
|
self.safe_after(self._refresh_kubectl_status_tab, delay=50)
|
|
|
|
# Run button + status label
|
|
btn_y = nb_y + nb_height + 12
|
|
self._init_scripts_button = tk.Button(
|
|
self.bg_canvas,
|
|
text="Run Scripts",
|
|
command=self.run_init_scripts,
|
|
bg="#F5F5DC",
|
|
fg="black",
|
|
activebackground="#E5E5D5",
|
|
highlightbackground="#F5F5DC",
|
|
highlightthickness=0,
|
|
relief="flat",
|
|
font=("SF Pro Text", 11),
|
|
padx=16,
|
|
pady=8,
|
|
)
|
|
btn_window = self.bg_canvas.create_window(
|
|
x_label, btn_y, window=self._init_scripts_button, anchor="nw", width=180
|
|
)
|
|
self._canvas_items.append(btn_window)
|
|
self._overlay_widgets.append(self._init_scripts_button)
|
|
|
|
self._init_scripts_status_label = ui.canvas_text(
|
|
self, x_label + 196, btn_y + 12, "", fill="black", font=("SF Pro Text", 12)
|
|
)
|
|
self._canvas_items.append(self._init_scripts_status_label)
|
|
|
|
# On page load: if previously completed check live state; else stay yellow
|
|
already_done = getattr(self, "_scripts_success", False) or (
|
|
(self.knoe_cfg_data.get("Initialization Scripts") or {}).get("STATUS") == "Completed"
|
|
)
|
|
if already_done:
|
|
self.safe_after(self._check_init_scripts_status_async)
|
|
|
|
def run_init_scripts(self):
|
|
self._action_flags["init_scripts.run_scripts"] = True
|
|
|
|
def worker():
|
|
# Map script filenames → category tab key
|
|
_SCRIPT_TO_TAB = {
|
|
"init_common_services.sh": "tab_common",
|
|
"common_services_log": "tab_common",
|
|
"init_cloudnative_pg.sh": "tab_database",
|
|
"init_cnpg_backup.sh": "tab_database",
|
|
"init_kong.sh": "tab_platform",
|
|
"init_monitoring.sh": "tab_platform",
|
|
"init_nginx_ingress.sh": "tab_platform",
|
|
"init_port_forwards.sh": "tab_platform",
|
|
}
|
|
|
|
def _select_tab(script_name):
|
|
cat = _SCRIPT_TO_TAB.get(script_name, script_name)
|
|
idx = getattr(self, "_script_tab_index", {}).get(cat)
|
|
if idx is None:
|
|
return
|
|
self.safe_after(
|
|
lambda: (
|
|
self.script_tabs.select(idx)
|
|
if self.script_tabs.winfo_exists()
|
|
else None
|
|
)
|
|
)
|
|
|
|
def _con(script_name):
|
|
"""Return the console for a script (mapped to category tab)."""
|
|
cat = _SCRIPT_TO_TAB.get(script_name, script_name)
|
|
return self.script_consoles.get(cat) or self.script_consoles.get(script_name)
|
|
|
|
# Reset all traffic lights to yellow at start of run
|
|
for _lk in ("common_services", "knoe_db_1", "knoe_db_2", "knoe_db_3",
|
|
"barman_cloud", "monitoring", "kong"):
|
|
self.safe_after(lambda k=_lk: self._set_init_light(k, "yellow"))
|
|
|
|
self.safe_after(
|
|
lambda: (
|
|
self._init_scripts_button.configure(state="disabled")
|
|
if self._init_scripts_button.winfo_exists()
|
|
else None
|
|
)
|
|
)
|
|
self.safe_after(
|
|
lambda: (
|
|
self.bg_canvas.itemconfig(
|
|
self._init_scripts_status_label,
|
|
text="Running scripts...",
|
|
fill="blue",
|
|
)
|
|
if self.bg_canvas.winfo_exists()
|
|
else None
|
|
)
|
|
)
|
|
|
|
password = self.db_password.get()
|
|
|
|
# Prepare environment for scripts
|
|
env = os.environ.copy()
|
|
env["KNOE_HOME"] = str(PROJECT_ROOT)
|
|
env["KNOE_SERVICE"] = str(PROJECT_ROOT)
|
|
env["KNOE_DB_USER"] = self.db_username.get()
|
|
env["DB_PASSWORD"] = password
|
|
env["GRAFANA_ADMIN_PASSWORD"] = password
|
|
db_namespace = (self.db_namespace.get() or "").strip()
|
|
if not db_namespace:
|
|
db_namespace = (
|
|
((getattr(self, "knoe_cfg_data", {}) or {}).get("Global", {}) or {})
|
|
.get("DATABASE_NAMESPACE", "")
|
|
.strip()
|
|
)
|
|
if not db_namespace:
|
|
db_namespace = "default"
|
|
|
|
env["DATABASE_NAMESPACE"] = db_namespace
|
|
env["NAMESPACE"] = db_namespace
|
|
env["PROLE_NAMESPACE"] = db_namespace
|
|
env["SERVICE_NAMESPACE"] = self._get_service_namespace()
|
|
if self.kerberos_realm.get().strip():
|
|
env["KRB5_REALM"] = self.kerberos_realm.get().strip()
|
|
env["REALM"] = self.kerberos_realm.get().strip()
|
|
env["DOMAIN"] = self.kerberos_realm.get().strip().lower()
|
|
if self.kerberos_kdc.get().strip():
|
|
env["KRB5_KDC"] = self.kerberos_kdc.get().strip()
|
|
env["KRB5_ADMIN"] = self.kerberos_kdc.get().strip()
|
|
if self.kerberos_user.get().strip():
|
|
env["KRB5_USER"] = self.kerberos_user.get().strip()
|
|
if self.kerberos_password.get().strip():
|
|
env["KRB5_PASSWORD"] = self.kerberos_password.get().strip()
|
|
env["KERBEROS_ENABLED"] = _bool_str(self.kerberos_enabled.get())
|
|
env["ENABLED"] = env["KERBEROS_ENABLED"]
|
|
mode = self._deployment_mode()
|
|
if mode:
|
|
env["KNOE_MODE"] = mode
|
|
env["DEPLOYMENT_MODE"] = mode
|
|
env["DEPLOYMENT_TARGET"] = _deployment_target_label(
|
|
self.cluster_env.get()
|
|
)
|
|
if mode == "k8s":
|
|
for _k, _v in self._build_gke_registry_env().items():
|
|
env[_k] = _v
|
|
# Inject explicit cluster contexts so all kubectl/helm ops use the right cluster.
|
|
# Common services (garage, openbao, kong, monitoring) → APP cluster.
|
|
# CNPG + backup → DB cluster (cnpg_env below).
|
|
_app_ctx = (
|
|
self._cluster_kubecontext("app")
|
|
or (self.knoe_cfg_data.get("Global", {}) or {}).get(
|
|
"APP_CLUSTER_KUBECONTEXT", ""
|
|
)
|
|
).strip()
|
|
_db_ctx = (
|
|
self._cluster_kubecontext("db")
|
|
or (self.knoe_cfg_data.get("Global", {}) or {}).get(
|
|
"DB_CLUSTER_KUBECONTEXT", ""
|
|
)
|
|
).strip()
|
|
if _app_ctx:
|
|
env["APP_CLUSTER_KUBECONTEXT"] = _app_ctx
|
|
env["KUBECONTEXT"] = _app_ctx # default = app cluster
|
|
if _db_ctx:
|
|
env["DB_CLUSTER_KUBECONTEXT"] = _db_ctx
|
|
cnpg_env = dict(env)
|
|
if _db_ctx:
|
|
cnpg_env["KUBECONTEXT"] = _db_ctx
|
|
else:
|
|
cnpg_env = dict(env)
|
|
mode_args = ["--mode", mode] if mode else []
|
|
|
|
raw_min = (
|
|
(getattr(self, "knoe_cfg_data", {}) or {})
|
|
.get("Global", {})
|
|
.get(POLICY_CFG_KEY, "")
|
|
)
|
|
try:
|
|
min_nodes = int(
|
|
str(raw_min).strip()
|
|
or str(OPTIONAL_WORKLOADS_MIN_READY_SCHEDULABLE_NODES)
|
|
)
|
|
except Exception:
|
|
min_nodes = OPTIONAL_WORKLOADS_MIN_READY_SCHEDULABLE_NODES
|
|
opt_allowed, _count, opt_reason = evaluate_optional_workloads_allowed(
|
|
env=env, min_nodes=min_nodes
|
|
)
|
|
|
|
logs_dir = self._resolve_knoe_logs_dir()
|
|
try:
|
|
logs_dir.mkdir(parents=True, exist_ok=True)
|
|
except Exception:
|
|
pass
|
|
env.setdefault("PROLE_LOGS", str(logs_dir))
|
|
|
|
def _log_path_for(script_name: str) -> Path:
|
|
base = Path(script_name).stem
|
|
if base in ("common_services_log", "init_common_services"):
|
|
return logs_dir / "init_common_services.log"
|
|
return logs_dir / f"{base}.log"
|
|
|
|
# Ensure port-forward mappings and port-mapping.cfg are populated
|
|
# before init_nginx_ingress.sh runs (it reads port-mapping.cfg).
|
|
try:
|
|
self._sync_port_forward_mappings()
|
|
self._update_legacy_port_mapping()
|
|
except Exception:
|
|
pass
|
|
|
|
# 1. init_common_services.sh start
|
|
overall_success = True
|
|
if overall_success:
|
|
script = "init_common_services.sh"
|
|
_select_tab(script)
|
|
_con(script).clear()
|
|
_con(script).write(f"Running {script} start...\n")
|
|
|
|
svc_log_path = _log_path_for(script)
|
|
self._last_common_services_log_path = svc_log_path
|
|
env["COMMON_SERVICES_INIT_LOG"] = str(svc_log_path)
|
|
self._record_install_log(svc_log_path)
|
|
_con(script).write(f"Log file: {svc_log_path}\n\n")
|
|
|
|
try:
|
|
svc_fp = svc_log_path.open("a", encoding="utf-8")
|
|
except Exception:
|
|
svc_fp = None
|
|
|
|
def _svc_line(line):
|
|
_con("init_common_services.sh").write(line)
|
|
self._process_script_output_line(line)
|
|
if svc_fp:
|
|
try:
|
|
svc_fp.write(line)
|
|
svc_fp.flush()
|
|
except Exception:
|
|
pass
|
|
|
|
rc_svc = 0
|
|
svc_namespace = (
|
|
env.get("SERVICE_NAMESPACE")
|
|
or env.get("NAMESPACE")
|
|
or "default"
|
|
)
|
|
registry_ns = env.get("REGISTRY_NAMESPACE") or svc_namespace
|
|
project_root = getattr(self.controller, "project_root", PROJECT_ROOT)
|
|
|
|
def _svc_log(msg: str) -> None:
|
|
_svc_line(msg if msg.endswith("\n") else msg + "\n")
|
|
|
|
try:
|
|
_svc_log(f"[INFO] Registry update namespace={registry_ns}")
|
|
registry_ops.update(
|
|
namespace=registry_ns,
|
|
env=env,
|
|
project_root=project_root,
|
|
mode=mode,
|
|
log=_svc_log,
|
|
)
|
|
_svc_log(f"[INFO] OpenBao update namespace={svc_namespace}")
|
|
openbao_ops.update(
|
|
namespace=svc_namespace,
|
|
env=env,
|
|
project_root=project_root,
|
|
mode=mode,
|
|
log=_svc_log,
|
|
)
|
|
_svc_log(f"[INFO] Garage update namespace={svc_namespace}")
|
|
garage_store_ops.update(
|
|
namespace=svc_namespace,
|
|
env=env,
|
|
project_root=project_root,
|
|
mode=mode,
|
|
log=_svc_log,
|
|
)
|
|
_svc_log(f"[INFO] OpenTofu update namespace={svc_namespace}")
|
|
opentofu_ops.update(
|
|
namespace=svc_namespace,
|
|
env=env,
|
|
project_root=project_root,
|
|
mode=mode,
|
|
log=_svc_log,
|
|
)
|
|
except Exception as _svc_exc:
|
|
_svc_log(f"[ERROR] Common services Python execution failed: {_svc_exc}")
|
|
rc_svc = 1
|
|
if svc_fp:
|
|
try:
|
|
svc_fp.close()
|
|
except Exception:
|
|
pass
|
|
|
|
if rc_svc != 0:
|
|
_con(script).write(
|
|
f"\nERROR: {script} start failed with code {rc_svc}\n"
|
|
)
|
|
overall_success = False
|
|
self.safe_after(lambda: self._set_init_light("common_services", "red"))
|
|
else:
|
|
_con(script).write(f"\n{script} completed successfully.\n")
|
|
self.safe_after(lambda: self._set_init_light("common_services", "green"))
|
|
else:
|
|
_con("init_common_services.sh").write(
|
|
"Skipping common services initialization because previous steps failed.\n"
|
|
)
|
|
|
|
# 2. init_monitoring.sh initialize — runs before CNPG so PodMonitor CRD is present
|
|
# when the CNPG cluster is created (enablePodMonitor: true in knoe-db.yaml).
|
|
if overall_success and opt_allowed:
|
|
script = "init_monitoring.sh"
|
|
_select_tab(script)
|
|
_con(script).clear()
|
|
_con(script).write(f"Running {script} initialize...\n")
|
|
|
|
log_path = _log_path_for(script)
|
|
self._record_install_log(log_path)
|
|
try:
|
|
log_fp = log_path.open("a", encoding="utf-8")
|
|
except Exception:
|
|
log_fp = None
|
|
|
|
def _monitoring_line(line):
|
|
_con("init_monitoring.sh").write(line)
|
|
self._process_script_output_line(line)
|
|
if log_fp:
|
|
try:
|
|
log_fp.write(line)
|
|
log_fp.flush()
|
|
except Exception:
|
|
pass
|
|
# Extract Grafana password if present
|
|
if "GRAFANA_ADMIN_PASSWORD=" in line:
|
|
pwd = line.split("GRAFANA_ADMIN_PASSWORD=")[1].strip()
|
|
if pwd:
|
|
mon = self.knoe_cfg_data.get("Monitoring", {})
|
|
if mon is None:
|
|
mon = {}
|
|
mon["GRAFANA_ADMIN_PASSWORD"] = pwd
|
|
self.knoe_cfg_data["Monitoring"] = mon
|
|
self._save_knoe_cfg()
|
|
|
|
rc_mon = 0
|
|
monitoring_ns = env.get("MONITORING_NAMESPACE") or "monitoring"
|
|
|
|
def _monitoring_log(msg: str) -> None:
|
|
_monitoring_line(msg if msg.endswith("\n") else msg + "\n")
|
|
|
|
try:
|
|
monitoring_ops.initialize(
|
|
namespace=monitoring_ns,
|
|
env=env,
|
|
mode=mode,
|
|
log=_monitoring_log,
|
|
)
|
|
except Exception as _mon_exc:
|
|
_monitoring_log(f"[ERROR] Monitoring Python execution failed: {_mon_exc}")
|
|
rc_mon = 1
|
|
if log_fp:
|
|
try:
|
|
log_fp.close()
|
|
except Exception:
|
|
pass
|
|
|
|
if rc_mon != 0:
|
|
_con(script).write(
|
|
f"\nERROR: {script} initialize failed with code {rc_mon}\n"
|
|
)
|
|
overall_success = False
|
|
self.safe_after(lambda: self._set_init_light("monitoring", "red"))
|
|
else:
|
|
mon = self.knoe_cfg_data.get("Monitoring", {}) or {}
|
|
if not str(mon.get("GRAFANA_ADMIN_PASSWORD") or "").strip():
|
|
mon["GRAFANA_ADMIN_PASSWORD"] = env.get("GRAFANA_ADMIN_PASSWORD", "")
|
|
self.knoe_cfg_data["Monitoring"] = mon
|
|
try:
|
|
self._save_knoe_cfg()
|
|
except Exception:
|
|
pass
|
|
_con(script).write(f"\n{script} completed successfully.\n")
|
|
self.safe_after(lambda: self._set_init_light("monitoring", "green"))
|
|
elif overall_success and not opt_allowed:
|
|
script = "init_monitoring.sh"
|
|
_select_tab(script)
|
|
_con(script).clear()
|
|
_con(script).write(f"[SKIP] Monitoring disabled by policy: {opt_reason}\n")
|
|
mon = (getattr(self, "knoe_cfg_data", {}) or {}).get("Monitoring") or {}
|
|
mon["STATUS"] = "Skipped"
|
|
self.knoe_cfg_data["Monitoring"] = mon
|
|
try:
|
|
self._save_knoe_cfg()
|
|
except Exception:
|
|
pass
|
|
self.safe_after(lambda: self._set_init_light("monitoring", "green"))
|
|
else:
|
|
_con("init_monitoring.sh").write(
|
|
"Skipping Monitoring initialization because previous steps failed.\n"
|
|
)
|
|
|
|
# 3. CloudNative-PG initialization (Python)
|
|
cnpg_script = "init_cloudnative_pg.sh"
|
|
if overall_success:
|
|
_con("init_monitoring.sh").write(
|
|
"\nMonitoring done — running database initialization (see Database tab)...\n"
|
|
)
|
|
_select_tab(cnpg_script)
|
|
_con(cnpg_script).clear()
|
|
_con(cnpg_script).write("Running CloudNative-PG initialization...\n")
|
|
|
|
log_path = _log_path_for(cnpg_script)
|
|
self._record_install_log(log_path)
|
|
try:
|
|
log_fp = log_path.open("a", encoding="utf-8")
|
|
except Exception:
|
|
log_fp = None
|
|
|
|
def _cnpg_log(msg):
|
|
line = msg if msg.endswith("\n") else msg + "\n"
|
|
_con("init_cloudnative_pg.sh").write(line)
|
|
self._process_script_output_line(line)
|
|
if log_fp:
|
|
try:
|
|
log_fp.write(line)
|
|
log_fp.flush()
|
|
except Exception:
|
|
pass
|
|
|
|
cnpg_ns = env.get("DATABASE_NAMESPACE") or env.get("NAMESPACE", "default")
|
|
cnpg_cluster = env.get("CLUSTER_NAME") or env.get("CNPG_CLUSTER_NAME") or "knoe-db"
|
|
cnpg_project_root = getattr(self.controller, "project_root", PROJECT_ROOT)
|
|
try:
|
|
cnpg_initialize(
|
|
namespace=cnpg_ns,
|
|
cluster_name=cnpg_cluster,
|
|
env=cnpg_env,
|
|
project_root=cnpg_project_root,
|
|
log=_cnpg_log,
|
|
mode=mode,
|
|
)
|
|
_con(cnpg_script).write(
|
|
"\nCloudNative-PG initialization completed successfully.\n"
|
|
)
|
|
for _lk in ("knoe_db_1", "knoe_db_2", "knoe_db_3"):
|
|
self.safe_after(lambda k=_lk: self._set_init_light(k, "green"))
|
|
except Exception as _cnpg_exc:
|
|
_con(cnpg_script).write(
|
|
f"\nERROR: CloudNative-PG initialization failed: {_cnpg_exc}\n"
|
|
)
|
|
overall_success = False
|
|
for _lk in ("knoe_db_1", "knoe_db_2", "knoe_db_3"):
|
|
self.safe_after(lambda k=_lk: self._set_init_light(k, "red"))
|
|
finally:
|
|
if log_fp:
|
|
try:
|
|
log_fp.close()
|
|
except Exception:
|
|
pass
|
|
else:
|
|
_con("init_cloudnative_pg.sh").write(
|
|
"Skipping CloudNative-PG initialization because previous steps failed.\n"
|
|
)
|
|
|
|
# 4. init_cnpg_backup.sh start
|
|
if overall_success:
|
|
script = "init_cnpg_backup.sh"
|
|
_select_tab(script)
|
|
_con(script).clear()
|
|
_con(script).write(f"Running {script} start...\n")
|
|
|
|
log_path = _log_path_for(script)
|
|
self._record_install_log(log_path)
|
|
try:
|
|
log_fp = log_path.open("a", encoding="utf-8")
|
|
except Exception:
|
|
log_fp = None
|
|
|
|
def _backup_line(line):
|
|
_con("init_cnpg_backup.sh").write(line)
|
|
self._process_script_output_line(line)
|
|
if log_fp:
|
|
try:
|
|
log_fp.write(line)
|
|
log_fp.flush()
|
|
except Exception:
|
|
pass
|
|
|
|
rc_backup = self.controller.run_script(
|
|
script, args=mode_args + ["start"], env=cnpg_env, on_line=_backup_line
|
|
)
|
|
if log_fp:
|
|
try:
|
|
log_fp.close()
|
|
except Exception:
|
|
pass
|
|
|
|
if rc_backup != 0:
|
|
_con(script).write(f"\nERROR: {script} start failed with code {rc_backup}\n")
|
|
overall_success = False
|
|
self.safe_after(lambda: self._set_init_light("barman_cloud", "red"))
|
|
else:
|
|
_con(script).write(f"\n{script} completed successfully.\n")
|
|
self.safe_after(lambda: self._set_init_light("barman_cloud", "green"))
|
|
else:
|
|
_con("init_cnpg_backup.sh").write(
|
|
f"Skipping {cnpg_cluster} backup because previous steps failed.\n"
|
|
)
|
|
|
|
# 5. init_kong.sh start
|
|
if overall_success:
|
|
script = "init_kong.sh"
|
|
_select_tab(script)
|
|
_con(script).clear()
|
|
_con(script).write(f"Running {script} start...\n")
|
|
|
|
log_path = _log_path_for(script)
|
|
self._record_install_log(log_path)
|
|
try:
|
|
log_fp = log_path.open("a", encoding="utf-8")
|
|
except Exception:
|
|
log_fp = None
|
|
|
|
def _kong_line(line):
|
|
_con("init_kong.sh").write(line)
|
|
self._process_script_output_line(line)
|
|
if log_fp:
|
|
try:
|
|
log_fp.write(line)
|
|
log_fp.flush()
|
|
except Exception:
|
|
pass
|
|
|
|
_svc_ns = (env.get("SERVICE_NAMESPACE") or "").strip()
|
|
if not _svc_ns or _svc_ns == "default":
|
|
_svc_ns = (self._get_service_namespace() or "").strip()
|
|
kong_ns = _svc_ns if (_svc_ns and _svc_ns != "default") else "knoe-system"
|
|
rc_kong = self.controller.run_script(
|
|
script, args=mode_args + ["-n", kong_ns, "start"], env=env, on_line=_kong_line
|
|
)
|
|
if log_fp:
|
|
try:
|
|
log_fp.close()
|
|
except Exception:
|
|
pass
|
|
|
|
if rc_kong != 0:
|
|
_con(script).write(f"\nERROR: {script} start failed with code {rc_kong}\n")
|
|
overall_success = False
|
|
else:
|
|
_con("init_kong.sh").write(
|
|
"Skipping Kong because previous steps failed.\n"
|
|
)
|
|
|
|
# 6. init_nginx_ingress.sh initialize (k3s only — not used in k3d)
|
|
if overall_success and mode != "k3d":
|
|
script = "init_nginx_ingress.sh"
|
|
_select_tab(script)
|
|
_con(script).clear()
|
|
_con(script).write(f"Running {script} initialize...\n")
|
|
log_path = _log_path_for(script)
|
|
self._record_install_log(log_path)
|
|
try:
|
|
log_fp = log_path.open("a", encoding="utf-8")
|
|
except Exception:
|
|
log_fp = None
|
|
|
|
def _ingress_line(line):
|
|
_con("init_nginx_ingress.sh").write(line)
|
|
self._process_script_output_line(line)
|
|
if log_fp:
|
|
try:
|
|
log_fp.write(line)
|
|
log_fp.flush()
|
|
except Exception:
|
|
pass
|
|
|
|
rc_ing = self.controller.run_script(
|
|
script, args=mode_args + ["initialize"], env=env, on_line=_ingress_line,
|
|
)
|
|
if log_fp:
|
|
try:
|
|
log_fp.close()
|
|
except Exception:
|
|
pass
|
|
if rc_ing != 0:
|
|
_con(script).write(f"\nERROR: {script} initialize failed with code {rc_ing}\n")
|
|
overall_success = False
|
|
self.safe_after(lambda: self._set_init_light("kong", "red"))
|
|
else:
|
|
_con(script).write(f"\n{script} completed successfully.\n")
|
|
self.safe_after(lambda: self._set_init_light("kong", "green"))
|
|
elif mode != "k3d":
|
|
_con("init_nginx_ingress.sh").write(
|
|
"Skipping Nginx Ingress because previous steps failed.\n"
|
|
)
|
|
|
|
# 7. init_port_forwards.sh start (k3d only — not used in k3s)
|
|
# Port-forwards are a post-installation convenience and are non-fatal:
|
|
# they only cover services deployed in this run, and optional services
|
|
# (argocd, dashboard) may not be installed yet.
|
|
if overall_success and mode != "k3s":
|
|
script = "init_port_forwards.sh"
|
|
_select_tab(script)
|
|
_con(script).clear()
|
|
_con(script).write(f"Running {script} start...\n")
|
|
|
|
def _pf_line(line):
|
|
_con("init_port_forwards.sh").write(line)
|
|
self._process_script_output_line(line)
|
|
|
|
env_pf = dict(env)
|
|
env_pf.setdefault("PORT_FORWARD_SKIP_VALIDATE", "1")
|
|
env_pf.setdefault("PORT_FORWARD_SKIP_WAIT", "1")
|
|
env_pf.setdefault("PORT_FORWARD_WAIT_TIMEOUT", "20")
|
|
env_pf.setdefault("PORT_FORWARD_WAIT_INTERVAL", "2")
|
|
|
|
rc_pf = self.controller.run_script(
|
|
script, args=["--force", "--mode", mode, "start"],
|
|
env=env_pf, on_line=_pf_line,
|
|
)
|
|
if rc_pf != 0:
|
|
_con(script).write(
|
|
f"\n[WARN] {script} start exited {rc_pf} — port-forwards may not be running.\n"
|
|
"This is non-fatal; re-run 'init_port_forwards.sh start' manually after install.\n"
|
|
)
|
|
else:
|
|
_con(script).write("\nPort-forwards started.\n")
|
|
self.safe_after(lambda: self._set_init_light("kong", "green"))
|
|
|
|
# Verify critical secrets
|
|
ns = (
|
|
str(env.get("DATABASE_NAMESPACE") or env.get("NAMESPACE") or "default").strip()
|
|
or "default"
|
|
)
|
|
kubectl_base_cmd = ["kubectl"]
|
|
try:
|
|
resolved_base_cmd = list(self._kubectl_base_cmd(mode))
|
|
if resolved_base_cmd:
|
|
kubectl_base_cmd = resolved_base_cmd
|
|
except Exception:
|
|
pass
|
|
_con("init_cloudnative_pg.sh").write(
|
|
f"\nVerifying critical secrets in namespace {ns}...\n"
|
|
)
|
|
critical_secrets = ["knoe-db-user", "knoe-db-superuser", "cnpg-admin-key"]
|
|
missing_secrets = []
|
|
for secret in critical_secrets:
|
|
rc_s, _ = self._run_cmd_capture(
|
|
kubectl_base_cmd + ["get", "secret", secret, "-n", ns]
|
|
)
|
|
if rc_s != 0:
|
|
missing_secrets.append(secret)
|
|
|
|
has_secret_check_warnings = False
|
|
if missing_secrets:
|
|
has_secret_check_warnings = True
|
|
_con("init_cloudnative_pg.sh").write(
|
|
f"WARNING: Missing secrets in namespace '{ns}': {', '.join(missing_secrets)}.\n"
|
|
)
|
|
_con("init_cloudnative_pg.sh").write(
|
|
"Continuing because initialization scripts succeeded; verify CNPG secrets before production use.\n"
|
|
)
|
|
|
|
if overall_success:
|
|
status_text = "Initialization complete!"
|
|
status_fill = "#34c759"
|
|
if has_secret_check_warnings:
|
|
status_text = "Initialization complete (warnings)."
|
|
status_fill = "#ff9f0a"
|
|
self.safe_after(
|
|
lambda: (
|
|
self.bg_canvas.itemconfig(
|
|
self._init_scripts_status_label,
|
|
text=status_text,
|
|
fill=status_fill,
|
|
)
|
|
if self.bg_canvas.winfo_exists()
|
|
else None
|
|
)
|
|
)
|
|
self._scripts_success = True
|
|
else:
|
|
self.safe_after(
|
|
lambda: (
|
|
self.bg_canvas.itemconfig(
|
|
self._init_scripts_status_label,
|
|
text="Initialization failed.",
|
|
fill="#ff3b30",
|
|
)
|
|
if self.bg_canvas.winfo_exists()
|
|
else None
|
|
)
|
|
)
|
|
self._scripts_success = False
|
|
|
|
try:
|
|
self.knoe_cfg_data["Initialization Scripts"]["STATUS"] = (
|
|
"Completed" if self._scripts_success else "Attempted"
|
|
)
|
|
self.safe_after(self._save_knoe_cfg)
|
|
except Exception:
|
|
pass
|
|
|
|
self.safe_after(lambda: self.update_footer())
|
|
self.safe_after(
|
|
lambda: (
|
|
self._init_scripts_button.configure(state="normal")
|
|
if self._init_scripts_button.winfo_exists()
|
|
else None
|
|
)
|
|
)
|
|
self.safe_after(self.check_services_status_async)
|
|
|
|
threading.Thread(target=worker, daemon=True).start()
|
|
|
|
def _on_init_scripts_tab_changed(self, _event=None):
|
|
try:
|
|
current_idx = self.script_tabs.index("current")
|
|
except Exception:
|
|
return
|
|
if current_idx == self._script_tab_index.get(self.KUBECTL_STATUS_TAB):
|
|
self._refresh_kubectl_status_tab()
|
|
|
|
def _refresh_kubectl_status_tab(self):
|
|
console = self.script_consoles.get(self.KUBECTL_STATUS_TAB)
|
|
if not console:
|
|
return
|
|
|
|
namespace = (
|
|
(self.db_namespace.get() or "").strip()
|
|
or os.environ.get("NAMESPACE")
|
|
or "default"
|
|
)
|
|
base_cmd = self._kubectl_base_cmd()
|
|
|
|
def _display_cmd(args: list[str]) -> str:
|
|
redacted = []
|
|
for item in base_cmd + args:
|
|
if item.startswith("--token="):
|
|
redacted.append("--token=***")
|
|
else:
|
|
redacted.append(item)
|
|
return " ".join(shlex.quote(x) for x in redacted)
|
|
|
|
def _run_kubectl(args: list[str]):
|
|
env = os.environ.copy()
|
|
env.setdefault("KNOE_HOME", str(PROJECT_ROOT))
|
|
try:
|
|
res = subprocess.run(
|
|
base_cmd + args, capture_output=True, text=True, env=env
|
|
)
|
|
out = res.stdout or ""
|
|
err = res.stderr or ""
|
|
return res.returncode, out, err
|
|
except Exception as exc:
|
|
return 1, "", f"{exc}"
|
|
|
|
def worker():
|
|
cmd_args = ["get", "pods", "-A", "-o", "wide"]
|
|
code, out, err = _run_kubectl(cmd_args)
|
|
now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
|
header = f"# {_display_cmd(cmd_args)} ({now}) [highlight: namespace={namespace}]"
|
|
|
|
lines = [ln.rstrip("\n") for ln in out.splitlines() if ln.strip() != ""]
|
|
if code != 0 and not lines:
|
|
lines = ["(no output)"]
|
|
|
|
pod_rows = []
|
|
unhappy = []
|
|
if lines:
|
|
for idx, line in enumerate(lines):
|
|
if idx == 0:
|
|
pod_rows.append({"raw": line, "ns": "", "unhappy": False})
|
|
continue
|
|
parts = line.split()
|
|
if len(parts) < 4:
|
|
pod_rows.append({"raw": line, "ns": "", "unhappy": False})
|
|
continue
|
|
ns, name, ready, status = parts[0], parts[1], parts[2], parts[3]
|
|
|
|
status_lc = status.lower()
|
|
happy_status = status_lc in ("running", "completed", "succeeded")
|
|
ready_ok = True
|
|
if status_lc == "running" and "/" in ready:
|
|
try:
|
|
a, b = ready.split("/", 1)
|
|
ready_ok = a == b
|
|
except Exception:
|
|
ready_ok = False
|
|
|
|
is_happy = happy_status and (
|
|
ready_ok or status_lc in ("completed", "succeeded")
|
|
)
|
|
if not is_happy:
|
|
unhappy.append(
|
|
{"ns": ns, "name": name, "status": status, "ready": ready}
|
|
)
|
|
|
|
pod_rows.append({"raw": line, "ns": ns, "unhappy": not is_happy})
|
|
|
|
details = ""
|
|
if unhappy:
|
|
blocks = ["", "# Unhealthy pods detected; collecting events + logs"]
|
|
for pod in unhappy:
|
|
ns = pod["ns"]
|
|
name = pod["name"]
|
|
blocks.append(f"\n## Events for {ns}/{name}")
|
|
ec, eout, eerr = _run_kubectl(
|
|
[
|
|
"get",
|
|
"events",
|
|
"-n",
|
|
ns,
|
|
"--field-selector",
|
|
f"involvedObject.kind=Pod,involvedObject.name={name}",
|
|
"--sort-by=.lastTimestamp",
|
|
]
|
|
)
|
|
blocks.append(eout.strip() or eerr.strip() or "(no events output)")
|
|
|
|
blocks.append(f"\n## Logs for {ns}/{name}")
|
|
lc, lout, lerr = _run_kubectl(
|
|
["logs", "-n", ns, name, "--all-containers", "--tail=200"]
|
|
)
|
|
blocks.append(lout.strip() or lerr.strip() or "(no logs output)")
|
|
|
|
details = "\n".join(blocks).rstrip() + "\n"
|
|
elif err.strip():
|
|
details = "\n# kubectl stderr\n" + err.strip() + "\n"
|
|
|
|
def render():
|
|
if not console.text.winfo_exists():
|
|
return
|
|
text = console.text
|
|
text.configure(state="normal")
|
|
text.delete("1.0", "end")
|
|
text.tag_configure("header", foreground="#6e6e73")
|
|
text.tag_configure("ns", background="#DDF4FF")
|
|
text.tag_configure("bad", foreground="#B00020")
|
|
|
|
text.insert("end", header + "\n\n", ("header",))
|
|
for row in pod_rows:
|
|
tags = []
|
|
if row["ns"] == namespace:
|
|
tags.append("ns")
|
|
if row["unhappy"]:
|
|
tags.append("bad")
|
|
if row["raw"].strip():
|
|
text.insert("end", row["raw"] + "\n", tuple(tags))
|
|
if details:
|
|
text.insert("end", "\n" + details)
|
|
text.see("1.0")
|
|
text.configure(state="disabled")
|
|
|
|
self.safe_after(render)
|
|
|
|
threading.Thread(target=worker, daemon=True).start()
|
|
|
|
def _check_init_scripts_status_async(self):
|
|
"""Query the live cluster and flip traffic lights to reflect current state.
|
|
|
|
Called on page load when a previous successful run is recorded, so the
|
|
user sees green lights without re-running the scripts.
|
|
"""
|
|
def worker():
|
|
ns = (
|
|
(self.db_namespace.get() or "").strip()
|
|
or os.environ.get("NAMESPACE")
|
|
or "default"
|
|
)
|
|
base_cmd = list(self._kubectl_base_cmd())
|
|
|
|
def _kubectl(*args):
|
|
try:
|
|
res = subprocess.run(
|
|
base_cmd + list(args),
|
|
capture_output=True, text=True,
|
|
env=os.environ.copy(),
|
|
)
|
|
return res.returncode, res.stdout, res.stderr
|
|
except Exception as exc:
|
|
return 1, "", str(exc)
|
|
|
|
# Common Services: any running pod in the service namespace
|
|
svc_ns = (self._get_service_namespace() or ns).strip() or ns
|
|
rc, out, _ = _kubectl(
|
|
"get", "pods", "-n", svc_ns,
|
|
"--field-selector=status.phase=Running", "-o", "name",
|
|
)
|
|
cs_ok = rc == 0 and bool(out.strip())
|
|
self.safe_after(
|
|
lambda ok=cs_ok: self._set_init_light("common_services", "green" if ok else "yellow")
|
|
)
|
|
|
|
# Database Services: knoe-db-1/2/3 pods
|
|
cnpg_cluster = (
|
|
os.environ.get("CNPG_CLUSTER_NAME")
|
|
or os.environ.get("CLUSTER_NAME")
|
|
or "knoe-db"
|
|
)
|
|
for i, key in enumerate(("knoe_db_1", "knoe_db_2", "knoe_db_3"), 1):
|
|
pod_name = f"{cnpg_cluster}-{i}"
|
|
rc_p, out_p, _ = _kubectl("get", "pod", pod_name, "-n", ns)
|
|
is_ready = rc_p == 0 and "Running" in out_p
|
|
self.safe_after(
|
|
lambda ok=is_ready, k=key: self._set_init_light(k, "green" if ok else "yellow")
|
|
)
|
|
|
|
# Barman-cloud: any completed backup
|
|
rc_b, out_b, _ = _kubectl(
|
|
"get", "backup", "-n", ns, "-o",
|
|
"jsonpath={.items[*].status.phase}",
|
|
)
|
|
barman_ok = rc_b == 0 and "completed" in out_b.lower()
|
|
self.safe_after(
|
|
lambda ok=barman_ok: self._set_init_light("barman_cloud", "green" if ok else "yellow")
|
|
)
|
|
|
|
# Monitoring: any running pod in monitoring namespace
|
|
mon_ns = os.environ.get("MONITORING_NAMESPACE") or "monitoring"
|
|
rc_m, out_m, _ = _kubectl(
|
|
"get", "pods", "-n", mon_ns,
|
|
"--field-selector=status.phase=Running", "-o", "name",
|
|
)
|
|
mon_ok = rc_m == 0 and bool(out_m.strip())
|
|
self.safe_after(
|
|
lambda ok=mon_ok: self._set_init_light("monitoring", "green" if ok else "yellow")
|
|
)
|
|
|
|
# Kong / Ingress: check kong or ingress-nginx namespace
|
|
kong_ok = False
|
|
for ns_check in ("kong", "ingress-nginx", ns):
|
|
rc_k, out_k, _ = _kubectl(
|
|
"get", "pods", "-n", ns_check,
|
|
"--field-selector=status.phase=Running", "-o", "name",
|
|
)
|
|
if rc_k == 0 and any(
|
|
"kong" in line or "ingress" in line
|
|
for line in out_k.splitlines()
|
|
):
|
|
kong_ok = True
|
|
break
|
|
self.safe_after(
|
|
lambda ok=kong_ok: self._set_init_light("kong", "green" if ok else "yellow")
|
|
)
|
|
|
|
threading.Thread(target=worker, daemon=True).start()
|
|
|
|
def _render_init_cnpg_deploy_page(self):
|
|
# Letterhead at top right
|
|
content_width = self.bg_canvas.winfo_width() or 975
|
|
right_margin = content_width - 48
|
|
|
|
ui.canvas_text(
|
|
self,
|
|
right_margin,
|
|
40,
|
|
"knoe.dev",
|
|
fill="#6e6e73",
|
|
font=("SF Pro Text", 32, "bold"),
|
|
anchor="ne",
|
|
)
|
|
ui.canvas_text(
|
|
self,
|
|
right_margin,
|
|
85,
|
|
"infrastructure.auto()",
|
|
fill="#6e6e73",
|
|
font=("SF Pro Text", 18),
|
|
anchor="ne",
|
|
)
|
|
|
|
self._render_title("Deployment", y=150)
|
|
self._render_paragraph(
|
|
"Deploy the CloudNative-PG operator and cluster manifests to Kubernetes.",
|
|
y=200,
|
|
)
|
|
|
|
# Output Console
|
|
self._cnpg_deploy_console = self._create_console_output(
|
|
y=260, title="Deployment Output", width=900, height=520
|
|
)
|
|
|
|
# Use tk.Button
|
|
self._cnpg_deploy_button = tk.Button(
|
|
self.bg_canvas,
|
|
text="Run Deployment",
|
|
command=self.run_cnpg_deploy,
|
|
bg="#F5F5DC",
|
|
fg="black",
|
|
activebackground="#E5E5D5",
|
|
highlightbackground="#F5F5DC",
|
|
highlightthickness=0,
|
|
relief="flat",
|
|
font=("SF Pro Text", 11),
|
|
padx=16,
|
|
pady=8,
|
|
)
|
|
self._cnpg_deploy_button.update_idletasks()
|
|
deploy_btn_height = self._cnpg_deploy_button.winfo_reqheight() or 32
|
|
|
|
# Deployment mode selector
|
|
mode_label = ui.canvas_text(
|
|
self, 48, 782, "Mode", fill="#6e6e73", font=("SF Pro Text", 10, "bold")
|
|
)
|
|
self._canvas_items.append(mode_label)
|
|
mode_values = [
|
|
"knoe-dev-cluster",
|
|
"knoe-service-cluster",
|
|
"knoe-prod-cluster",
|
|
]
|
|
mode_frame = tk.Frame(
|
|
self.bg_canvas, bg="#F5F5DC", width=180, height=deploy_btn_height
|
|
)
|
|
mode_frame.pack_propagate(False)
|
|
mode_combo = ttk.Combobox(
|
|
mode_frame,
|
|
textvariable=self.deploy_target,
|
|
values=mode_values,
|
|
state="readonly",
|
|
width=24,
|
|
)
|
|
mode_combo.pack(fill="both", expand=True)
|
|
mode_combo_window = self.bg_canvas.create_window(
|
|
48, 800, window=mode_frame, anchor="nw", width=180, height=deploy_btn_height
|
|
)
|
|
self._canvas_items.append(mode_combo_window)
|
|
self._overlay_widgets.append(mode_frame)
|
|
|
|
btn_window = self.bg_canvas.create_window(
|
|
240, 800, window=self._cnpg_deploy_button, anchor="nw", width=180
|
|
)
|
|
self._canvas_items.append(btn_window)
|
|
self._overlay_widgets.append(self._cnpg_deploy_button)
|
|
|
|
# Force Rollout Button
|
|
self._cnpg_rollout_button = tk.Button(
|
|
self.bg_canvas,
|
|
text="Force Rollout",
|
|
command=self.run_cnpg_rollout,
|
|
bg="#F5F5DC",
|
|
fg="black",
|
|
activebackground="#E5E5D5",
|
|
highlightbackground="#F5F5DC",
|
|
highlightthickness=0,
|
|
relief="flat",
|
|
font=("SF Pro Text", 11),
|
|
padx=16,
|
|
pady=8,
|
|
)
|
|
rollout_btn_window = self.bg_canvas.create_window(
|
|
420, 800, window=self._cnpg_rollout_button, anchor="nw", width=160
|
|
)
|
|
self._canvas_items.append(rollout_btn_window)
|
|
self._overlay_widgets.append(self._cnpg_rollout_button)
|
|
|
|
# Save Deployment Button (moved from Post Install)
|
|
self._save_deployment_button = tk.Button(
|
|
self.bg_canvas,
|
|
text="Save Deployment",
|
|
command=self.run_final_deployment,
|
|
bg="#F5F5DC",
|
|
fg="black",
|
|
activebackground="#E5E5D5",
|
|
highlightbackground="#F5F5DC",
|
|
highlightthickness=0,
|
|
relief="flat",
|
|
font=("SF Pro Text", 11),
|
|
padx=16,
|
|
pady=8,
|
|
)
|
|
save_btn_window = self.bg_canvas.create_window(
|
|
600, 800, window=self._save_deployment_button, anchor="nw", width=180
|
|
)
|
|
self._canvas_items.append(save_btn_window)
|
|
self._overlay_widgets.append(self._save_deployment_button)
|
|
|
|
# Status Labels (below the buttons row to avoid overlap)
|
|
self._cnpg_deploy_status_label = ui.canvas_text(
|
|
self, 48, 840, "", fill="black", font=("SF Pro Text", 12)
|
|
)
|
|
self._canvas_items.append(self._cnpg_deploy_status_label)
|
|
self._save_deployment_status_label = ui.canvas_text(
|
|
self, 600, 840, "", fill="black", font=("SF Pro Text", 12)
|
|
)
|
|
self._canvas_items.append(self._save_deployment_status_label)
|
|
|
|
def run_cnpg_deploy(self):
|
|
self._action_flags["init_cnpg_deploy.run_deploy"] = True
|
|
|
|
def worker():
|
|
self.safe_after(
|
|
lambda: (
|
|
self._cnpg_deploy_button.configure(state="disabled")
|
|
if self._cnpg_deploy_button.winfo_exists()
|
|
else None
|
|
)
|
|
)
|
|
self.safe_after(
|
|
lambda: (
|
|
self.bg_canvas.itemconfig(
|
|
self._cnpg_deploy_status_label, text="Deploying...", fill="blue"
|
|
)
|
|
if self.bg_canvas.winfo_exists()
|
|
else None
|
|
)
|
|
)
|
|
|
|
target_value = ""
|
|
try:
|
|
target_value = (self.deploy_target.get() or "").strip()
|
|
except Exception:
|
|
target_value = ""
|
|
if not target_value:
|
|
try:
|
|
target_value = (self.cluster_env.get() or "").strip()
|
|
except Exception:
|
|
target_value = ""
|
|
target_key = _normalize_cluster_env(target_value)
|
|
mode = _deployment_mode_from_env(target_value or self.cluster_env.get())
|
|
|
|
if target_key in ("service", "prod"):
|
|
url = self._deployment_pipeline_url(target_key)
|
|
self._cnpg_deploy_console.clear()
|
|
self._cnpg_deploy_console.write(
|
|
f"Opening deployment pipeline for {target_value or target_key}...\n"
|
|
)
|
|
self._cnpg_deploy_console.write(f"{url}\n")
|
|
try:
|
|
webbrowser.open(url)
|
|
except Exception:
|
|
pass
|
|
self.safe_after(
|
|
lambda: (
|
|
self.bg_canvas.itemconfig(
|
|
self._cnpg_deploy_status_label,
|
|
text="Deployment pipeline opened.",
|
|
fill="#34c759",
|
|
)
|
|
if self.bg_canvas.winfo_exists()
|
|
else None
|
|
)
|
|
)
|
|
self._cnpg_success = False
|
|
self.safe_after(
|
|
lambda: (
|
|
self._cnpg_deploy_button.configure(state="normal")
|
|
if self._cnpg_deploy_button.winfo_exists()
|
|
else None
|
|
)
|
|
)
|
|
return
|
|
|
|
etc_dir = PROJECT_ROOT / "etc"
|
|
# Prepare environment for scripts
|
|
env = os.environ.copy()
|
|
env["KNOE_HOME"] = str(PROJECT_ROOT)
|
|
env["KNOE_SERVICE"] = str(PROJECT_ROOT)
|
|
env["NAMESPACE"] = (self.db_namespace.get() or "").strip()
|
|
env["NAMESPACE"] = (self.db_namespace.get() or "").strip()
|
|
if mode:
|
|
env["KNOE_MODE"] = mode
|
|
env["DEPLOYMENT_MODE"] = mode
|
|
env["DEPLOYMENT_TARGET"] = _deployment_target_label(
|
|
self.cluster_env.get()
|
|
)
|
|
if mode == "k8s":
|
|
for _k, _v in self._build_gke_registry_env().items():
|
|
env[_k] = _v
|
|
|
|
ns = env.get("NAMESPACE", "default")
|
|
cluster_name = env.get("CLUSTER_NAME") or env.get("CNPG_CLUSTER_NAME") or "knoe-db"
|
|
cnpg_project_root = getattr(self.controller, "project_root", PROJECT_ROOT)
|
|
|
|
self._cnpg_deploy_console.clear()
|
|
self._cnpg_deploy_console.write("Starting CloudNative-PG deployment (Python)...\n")
|
|
|
|
def _deploy_log(msg):
|
|
line = msg if msg.endswith("\n") else msg + "\n"
|
|
self._cnpg_deploy_console.write(line)
|
|
|
|
try:
|
|
cnpg_deploy(
|
|
namespace=ns,
|
|
cluster_name=cluster_name,
|
|
env=env,
|
|
project_root=cnpg_project_root,
|
|
log=_deploy_log,
|
|
mode=mode,
|
|
)
|
|
self._cnpg_deploy_console.write("\nDeployment successful!\n")
|
|
self.safe_after(
|
|
lambda: (
|
|
self.bg_canvas.itemconfig(
|
|
self._cnpg_deploy_status_label,
|
|
text="Deployment successful!",
|
|
fill="#34c759",
|
|
)
|
|
if self.bg_canvas.winfo_exists()
|
|
else None
|
|
)
|
|
)
|
|
self._cnpg_success = True
|
|
except Exception as _deploy_exc:
|
|
self._cnpg_deploy_console.write(f"\nDeployment failed: {_deploy_exc}\n")
|
|
self.safe_after(
|
|
lambda: (
|
|
self.bg_canvas.itemconfig(
|
|
self._cnpg_deploy_status_label,
|
|
text=f"Deployment failed: {_deploy_exc}",
|
|
fill="#ff3b30",
|
|
)
|
|
if self.bg_canvas.winfo_exists()
|
|
else None
|
|
)
|
|
)
|
|
self._cnpg_success = False
|
|
|
|
self.safe_after(
|
|
lambda: (
|
|
self._cnpg_deploy_button.configure(state="normal")
|
|
if self._cnpg_deploy_button.winfo_exists()
|
|
else None
|
|
)
|
|
)
|
|
|
|
threading.Thread(target=worker, daemon=True).start()
|
|
|
|
def run_cnpg_rollout(self):
|
|
self._action_flags["init_cnpg_deploy.force_rollout"] = True
|
|
|
|
def worker():
|
|
self.safe_after(
|
|
lambda: (
|
|
self._cnpg_deploy_button.configure(state="disabled")
|
|
if self._cnpg_deploy_button.winfo_exists()
|
|
else None
|
|
)
|
|
)
|
|
self.safe_after(
|
|
lambda: (
|
|
self._cnpg_rollout_button.configure(state="disabled")
|
|
if self._cnpg_rollout_button.winfo_exists()
|
|
else None
|
|
)
|
|
)
|
|
self.safe_after(
|
|
lambda: (
|
|
self.bg_canvas.itemconfig(
|
|
self._cnpg_deploy_status_label,
|
|
text="Rolling out...",
|
|
fill="blue",
|
|
)
|
|
if self.bg_canvas.winfo_exists()
|
|
else None
|
|
)
|
|
)
|
|
|
|
# Prepare environment
|
|
env = os.environ.copy()
|
|
env["KNOE_HOME"] = str(PROJECT_ROOT)
|
|
env["KNOE_SERVICE"] = str(PROJECT_ROOT)
|
|
mode = self._deployment_mode()
|
|
if mode:
|
|
env["KNOE_MODE"] = mode
|
|
env["DEPLOYMENT_MODE"] = mode
|
|
env["DEPLOYMENT_TARGET"] = _deployment_target_label(
|
|
self.cluster_env.get()
|
|
)
|
|
ns = (self.db_namespace.get() or "").strip() or env.get("NAMESPACE", "default")
|
|
env["NAMESPACE"] = ns
|
|
cluster_name = env.get("CLUSTER_NAME") or env.get("CNPG_CLUSTER_NAME") or "knoe-db"
|
|
cnpg_project_root = getattr(self.controller, "project_root", PROJECT_ROOT)
|
|
|
|
self._cnpg_deploy_console.clear()
|
|
self._cnpg_deploy_console.write(
|
|
"Starting manual recreate rollout for knoe-db cluster (Python)...\n"
|
|
)
|
|
|
|
def _rollout_log(msg):
|
|
line = msg if msg.endswith("\n") else msg + "\n"
|
|
self._cnpg_deploy_console.write(line)
|
|
|
|
try:
|
|
cnpg_rollout(
|
|
namespace=ns,
|
|
cluster_name=cluster_name,
|
|
env=env,
|
|
log=_rollout_log,
|
|
)
|
|
self._cnpg_deploy_console.write("\nRollout successful!\n")
|
|
self.safe_after(
|
|
lambda: (
|
|
self.bg_canvas.itemconfig(
|
|
self._cnpg_deploy_status_label,
|
|
text="Rollout successful!",
|
|
fill="#34c759",
|
|
)
|
|
if self.bg_canvas.winfo_exists()
|
|
else None
|
|
)
|
|
)
|
|
except Exception as _rollout_exc:
|
|
self._cnpg_deploy_console.write(f"\nRollout failed: {_rollout_exc}\n")
|
|
self.safe_after(
|
|
lambda: (
|
|
self.bg_canvas.itemconfig(
|
|
self._cnpg_deploy_status_label,
|
|
text=f"Rollout failed: {_rollout_exc}",
|
|
fill="#ff3b30",
|
|
)
|
|
if self.bg_canvas.winfo_exists()
|
|
else None
|
|
)
|
|
)
|
|
|
|
self.safe_after(
|
|
lambda: (
|
|
self._cnpg_deploy_button.configure(state="normal")
|
|
if self._cnpg_deploy_button.winfo_exists()
|
|
else None
|
|
)
|
|
)
|
|
self.safe_after(
|
|
lambda: (
|
|
self._cnpg_rollout_button.configure(state="normal")
|
|
if self._cnpg_rollout_button.winfo_exists()
|
|
else None
|
|
)
|
|
)
|
|
|
|
def _run_service_layer_inline(self, next_page: str = "init_password"):
|
|
"""Run service-layer migration in the background without showing the
|
|
overlay screen. Used when all common services are already green so the
|
|
user can skip directly to Database Creation."""
|
|
|
|
def _inline_worker():
|
|
try:
|
|
namespace = (self.db_namespace.get() or "").strip() or "default"
|
|
env = self._script_env_for_namespace(namespace, cluster_role="app")
|
|
env["SERVICE_NAMESPACE"] = self._get_service_namespace()
|
|
rc = self.controller.run_script(
|
|
"init_service_layer.sh",
|
|
args=["migrate"],
|
|
env=env,
|
|
)
|
|
if rc == 0:
|
|
self._common_services_success = True
|
|
except Exception:
|
|
pass
|
|
# Navigate regardless — the service layer migration is best-effort
|
|
# when all traffic lights are already green.
|
|
self._common_services_success = True
|
|
self.safe_after(lambda: self.show_page(next_page))
|
|
|
|
threading.Thread(target=_inline_worker, daemon=True).start()
|
|
|
|
def _run_service_layer_overlay(self, next_page: str = "init_password"):
|
|
"""Ensure service-layer resources are ready before database creation."""
|
|
self._showing_service_overlay = True
|
|
self._common_services_success = False
|
|
self.update_footer()
|
|
self._clear_canvas_page()
|
|
|
|
# Letterhead at top right
|
|
content_width = self.bg_canvas.winfo_width() or 975
|
|
right_margin = content_width - 48
|
|
|
|
ui.canvas_text(
|
|
self,
|
|
right_margin,
|
|
40,
|
|
"knoe.dev",
|
|
fill="#6e6e73",
|
|
font=("SF Pro Text", 32, "bold"),
|
|
anchor="ne",
|
|
)
|
|
ui.canvas_text(
|
|
self,
|
|
right_margin,
|
|
85,
|
|
"infrastructure.auto()",
|
|
fill="#6e6e73",
|
|
font=("SF Pro Text", 18),
|
|
anchor="ne",
|
|
)
|
|
|
|
self._render_title("Preparing Service Layer", y=150)
|
|
self._render_paragraph(
|
|
"Ensuring service-layer resources are ready before database creation.",
|
|
y=200,
|
|
)
|
|
|
|
console = self._create_console_output(
|
|
y=260, title="Service Layer Output", width=900, height=520
|
|
)
|
|
status_label = ui.canvas_text(
|
|
self,
|
|
240,
|
|
812,
|
|
"Ready to deploy common services",
|
|
fill="black",
|
|
font=("SF Pro Text", 12),
|
|
)
|
|
self._canvas_items.append(status_label)
|
|
log_path = self._common_services_log_path()
|
|
try:
|
|
console.write(f"Log file: {log_path}\n\n")
|
|
except Exception:
|
|
pass
|
|
|
|
def worker():
|
|
try:
|
|
self.safe_after(
|
|
lambda: (
|
|
self._deploy_services_button.configure(state="disabled")
|
|
if self._deploy_services_button.winfo_exists()
|
|
else None
|
|
)
|
|
)
|
|
namespace = (self.db_namespace.get() or "").strip() or "default"
|
|
env = self._script_env_for_namespace(namespace, cluster_role="app")
|
|
env["SERVICE_NAMESPACE"] = self._get_service_namespace()
|
|
try:
|
|
log_fp = log_path.open("a", encoding="utf-8")
|
|
log_fp.write(
|
|
f"\n# Service layer deploy @ {time.strftime('%Y-%m-%d %H:%M:%S')}\n"
|
|
)
|
|
log_fp.flush()
|
|
except Exception:
|
|
log_fp = None
|
|
|
|
def _line(line: str):
|
|
console.write(line)
|
|
if log_fp:
|
|
try:
|
|
log_fp.write(line)
|
|
log_fp.flush()
|
|
except Exception:
|
|
pass
|
|
|
|
self.safe_after(
|
|
lambda: (
|
|
self.bg_canvas.itemconfig(
|
|
status_label, text="Deploying service layer...", fill="blue"
|
|
)
|
|
if self.bg_canvas.winfo_exists()
|
|
else None
|
|
)
|
|
)
|
|
rc = self.controller.run_script(
|
|
"init_service_layer.sh", args=["migrate"], env=env, on_line=_line
|
|
)
|
|
if log_fp:
|
|
try:
|
|
log_fp.write(f"\nExit status: {rc}\n")
|
|
log_fp.close()
|
|
except Exception:
|
|
pass
|
|
if rc != 0:
|
|
console.write(f"\nService layer setup failed (code {rc}).\n")
|
|
self.safe_after(
|
|
lambda: (
|
|
self.bg_canvas.itemconfig(
|
|
status_label,
|
|
text=f"Service layer failed (code {rc})",
|
|
fill="#ff3b30",
|
|
)
|
|
if self.bg_canvas.winfo_exists()
|
|
else None
|
|
)
|
|
)
|
|
self.safe_after(
|
|
lambda: (
|
|
self._deploy_services_button.configure(state="normal")
|
|
if self._deploy_services_button.winfo_exists()
|
|
else None
|
|
)
|
|
)
|
|
return
|
|
self.safe_after(
|
|
lambda: (
|
|
self.bg_canvas.itemconfig(
|
|
status_label,
|
|
text="Service layer ready. You may proceed.",
|
|
fill="#34c759",
|
|
)
|
|
if self.bg_canvas.winfo_exists()
|
|
else None
|
|
)
|
|
)
|
|
self._common_services_success = True
|
|
self.safe_after(self.update_footer)
|
|
except Exception as e:
|
|
console.write(f"\nService layer error: {e}\n")
|
|
self.safe_after(
|
|
lambda: (
|
|
self.bg_canvas.itemconfig(
|
|
status_label, text="Service layer error", fill="#ff3b30"
|
|
)
|
|
if self.bg_canvas.winfo_exists()
|
|
else None
|
|
)
|
|
)
|
|
self.safe_after(
|
|
lambda: (
|
|
self._deploy_services_button.configure(state="normal")
|
|
if self._deploy_services_button.winfo_exists()
|
|
else None
|
|
)
|
|
)
|
|
|
|
# Use tk.Button explicitly for control over button appearance and behavior
|
|
self._deploy_services_button = tk.Button(
|
|
self.bg_canvas,
|
|
text="Deploy Services",
|
|
command=lambda: threading.Thread(target=worker, daemon=True).start(),
|
|
bg="#F5F5DC",
|
|
fg="black",
|
|
activebackground="#E5E5D5",
|
|
highlightbackground="#F5F5DC",
|
|
highlightthickness=0,
|
|
relief="flat",
|
|
font=("SF Pro Text", 11),
|
|
padx=16,
|
|
pady=8,
|
|
)
|
|
btn_window = self.bg_canvas.create_window(
|
|
48, 800, window=self._deploy_services_button, anchor="nw", width=180
|
|
)
|
|
self._canvas_items.append(btn_window)
|
|
self._overlay_widgets.append(self._deploy_services_button)
|
|
|
|
def _run_preparation_overlay(self):
|
|
"""Prepare environment for database creation."""
|
|
self._action_flags["init_password.generate_ssh_key"] = True
|
|
self._clear_canvas_page()
|
|
|
|
# Letterhead at top right
|
|
content_width = self.bg_canvas.winfo_width() or 975
|
|
right_margin = content_width - 48
|
|
|
|
ui.canvas_text(
|
|
self,
|
|
right_margin,
|
|
40,
|
|
"knoe.dev",
|
|
fill="#6e6e73",
|
|
font=("SF Pro Text", 32, "bold"),
|
|
anchor="ne",
|
|
)
|
|
ui.canvas_text(
|
|
self,
|
|
right_margin,
|
|
85,
|
|
"infrastructure.auto()",
|
|
fill="#6e6e73",
|
|
font=("SF Pro Text", 18),
|
|
anchor="ne",
|
|
)
|
|
|
|
self._render_title("Preparing", y=150)
|
|
self._render_paragraph(
|
|
"Preparing your environment for database creation.", y=200
|
|
)
|
|
|
|
# Output Console - standardized to match Docker Build screen
|
|
console = self._create_console_output(y=260, title="", width=900, height=520)
|
|
|
|
# Status Label
|
|
status_label = ui.canvas_text(
|
|
self, 48, 812, "Initializing...", fill="black", font=("SF Pro Text", 12)
|
|
)
|
|
self._canvas_items.append(status_label)
|
|
|
|
def _show_return_button():
|
|
def _place():
|
|
try:
|
|
if getattr(self, "_prep_return_btn", None):
|
|
self.bg_canvas.delete(getattr(self, "_prep_return_btn_window", None))
|
|
except Exception:
|
|
pass
|
|
btn = tk.Button(
|
|
self.bg_canvas,
|
|
text="Back to Database",
|
|
command=lambda: self.show_page("init_password"),
|
|
bg="#F5F5DC",
|
|
fg="black",
|
|
activebackground="#E5E5D5",
|
|
highlightbackground="#F5F5DC",
|
|
highlightthickness=0,
|
|
relief="flat",
|
|
font=("SF Pro Text", 11),
|
|
padx=14,
|
|
pady=6,
|
|
)
|
|
btn_window = self.bg_canvas.create_window(
|
|
48, 840, window=btn, anchor="nw", width=180
|
|
)
|
|
self._prep_return_btn = btn
|
|
self._prep_return_btn_window = btn_window
|
|
self._overlay_widgets.append(btn)
|
|
self._canvas_items.append(btn_window)
|
|
|
|
self.safe_after(_place)
|
|
|
|
def worker():
|
|
key_path = Path.home() / ".ssh" / "id_knoe_ed25519"
|
|
key_path.parent.mkdir(parents=True, exist_ok=True)
|
|
password = self.db_password.get()
|
|
namespace = (self.db_namespace.get() or "").strip()
|
|
|
|
mode = _deployment_mode_from_env(self.cluster_env.get())
|
|
|
|
# Pre-pull dependent images into local registry before initialization
|
|
if mode == "k3s":
|
|
console.write(
|
|
"[INFO] Skipping image pre-pull in k3s mode (images are handled by the in-cluster registry/import).\n"
|
|
)
|
|
else:
|
|
try:
|
|
self.safe_after(
|
|
lambda: (
|
|
self.bg_canvas.itemconfig(
|
|
status_label, text="Pre-pulling images...", fill="blue"
|
|
)
|
|
if self.bg_canvas.winfo_exists()
|
|
else None
|
|
)
|
|
)
|
|
pull_ok = self._prepull_images_to_registry(
|
|
include_supabase=self.supabase_enabled.get(),
|
|
include_kerberos_proxy=self.kerberos_enabled.get(),
|
|
log=console.write,
|
|
)
|
|
if not pull_ok:
|
|
console.write(
|
|
"\n[WARN] Image pre-pull failed or incomplete. Continuing...\n"
|
|
)
|
|
except Exception as e:
|
|
console.write(f"\n[WARN] Image pre-pull error: {e}\n")
|
|
|
|
if key_path.exists():
|
|
console.write(f"Key already exists at {key_path}. Proceeding...\n")
|
|
self.safe_after(
|
|
lambda: (
|
|
self.bg_canvas.itemconfig(
|
|
status_label,
|
|
text="Keys present. Proceeding...",
|
|
fill="#34c759",
|
|
)
|
|
if self.bg_canvas.winfo_exists()
|
|
else None
|
|
)
|
|
)
|
|
time.sleep(0.5)
|
|
else:
|
|
self.safe_after(
|
|
lambda: (
|
|
self.bg_canvas.itemconfig(
|
|
status_label, text="Preparing keys...", fill="blue"
|
|
)
|
|
if self.bg_canvas.winfo_exists()
|
|
else None
|
|
)
|
|
)
|
|
cmd = [
|
|
"ssh-keygen",
|
|
"-t",
|
|
"ed25519",
|
|
"-N",
|
|
"",
|
|
"-f",
|
|
str(key_path),
|
|
"-C",
|
|
self.db_username.get(),
|
|
]
|
|
console.write(f"Initializing secure access: {' '.join(cmd)}\n\n")
|
|
|
|
proc = subprocess.Popen(
|
|
cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True
|
|
)
|
|
while True:
|
|
line = proc.stdout.readline()
|
|
if not line and proc.poll() is not None:
|
|
break
|
|
if line:
|
|
console.write(line)
|
|
|
|
if proc.returncode != 0:
|
|
console.write(
|
|
f"\nPreparation failed, trying alternative (code {proc.returncode})\n"
|
|
)
|
|
cmd = [
|
|
"ssh-keygen",
|
|
"-t",
|
|
"rsa",
|
|
"-b",
|
|
"4096",
|
|
"-N",
|
|
"",
|
|
"-f",
|
|
str(key_path),
|
|
"-C",
|
|
self.db_username.get(),
|
|
]
|
|
console.write(f"Initializing secure access: {' '.join(cmd)}\n\n")
|
|
proc = subprocess.Popen(
|
|
cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True
|
|
)
|
|
while True:
|
|
line = proc.stdout.readline()
|
|
if not line and proc.poll() is not None:
|
|
break
|
|
if line:
|
|
console.write(line)
|
|
|
|
if proc.returncode == 0:
|
|
console.write("\nPreparation completed successfully.\n")
|
|
self.safe_after(
|
|
lambda: (
|
|
self.bg_canvas.itemconfig(
|
|
status_label,
|
|
text="Preparation completed successfully.",
|
|
fill="#34c759",
|
|
)
|
|
if self.bg_canvas.winfo_exists()
|
|
else None
|
|
)
|
|
)
|
|
else:
|
|
console.write(
|
|
f"\nError during preparation (code {proc.returncode})\n"
|
|
)
|
|
self.safe_after(
|
|
lambda: (
|
|
self.bg_canvas.itemconfig(
|
|
status_label,
|
|
text=f"Error during preparation (code {proc.returncode})",
|
|
fill="#ff3b30",
|
|
)
|
|
if self.bg_canvas.winfo_exists()
|
|
else None
|
|
)
|
|
)
|
|
_show_return_button()
|
|
return
|
|
|
|
# Initialize OpenBao and store keys/passwords
|
|
self.safe_after(
|
|
lambda: (
|
|
self.bg_canvas.itemconfig(
|
|
status_label, text="Initializing OpenBao...", fill="blue"
|
|
)
|
|
if self.bg_canvas.winfo_exists()
|
|
else None
|
|
)
|
|
)
|
|
env = self._script_env_for_namespace(namespace, cluster_role="app")
|
|
env["KNOE_DB_USER"] = self.db_username.get().strip()
|
|
env["DB_PASSWORD"] = password
|
|
env["AT_REST_ENCRYPTION_ENABLED"] = _bool_str(
|
|
self.at_rest_encryption_enabled.get()
|
|
)
|
|
if self.at_rest_encryption_enabled.get():
|
|
console.write(
|
|
"At-rest encryption enabled: generating/storing TDE keys in OpenBao...\n"
|
|
)
|
|
rc = 0
|
|
try:
|
|
openbao_ops.initialize(
|
|
namespace=namespace,
|
|
env=env,
|
|
project_root=getattr(self.controller, "project_root", PROJECT_ROOT),
|
|
mode=mode,
|
|
log=lambda l: console.write(l if l.endswith("\n") else l + "\n"),
|
|
)
|
|
except Exception as _bao_exc:
|
|
rc = 1
|
|
console.write(f"\nOpenBao initialization failed: {_bao_exc}\n")
|
|
if rc != 0:
|
|
self.safe_after(
|
|
lambda: (
|
|
self.bg_canvas.itemconfig(
|
|
status_label,
|
|
text=f"OpenBao init failed (code {rc})",
|
|
fill="#ff3b30",
|
|
)
|
|
if self.bg_canvas.winfo_exists()
|
|
else None
|
|
)
|
|
)
|
|
_show_return_button()
|
|
return
|
|
|
|
# Materialize DB/CNPG secrets in Kubernetes using the user-entered password.
|
|
# This prevents later CNPG initialization from failing due to missing secrets.
|
|
self.safe_after(
|
|
lambda: (
|
|
self.bg_canvas.itemconfig(
|
|
status_label,
|
|
text="Ensuring database secrets...",
|
|
fill="blue",
|
|
)
|
|
if self.bg_canvas.winfo_exists()
|
|
else None
|
|
)
|
|
)
|
|
try:
|
|
self.ensure_db_k8s_secrets(
|
|
namespace,
|
|
password,
|
|
log_fn=lambda m: console.write(f"{m}\n") if m else None,
|
|
)
|
|
except Exception as e:
|
|
console.write(f"\nFailed to ensure database secrets: {e}\n")
|
|
self.safe_after(
|
|
lambda: (
|
|
self.bg_canvas.itemconfig(
|
|
status_label,
|
|
text="Database secrets error",
|
|
fill="#ff3b30",
|
|
)
|
|
if self.bg_canvas.winfo_exists()
|
|
else None
|
|
)
|
|
)
|
|
_show_return_button()
|
|
return
|
|
|
|
# Move to next page
|
|
self.safe_after(
|
|
lambda: (
|
|
self.bg_canvas.itemconfig(
|
|
status_label,
|
|
text="OpenBao ready. Proceeding...",
|
|
fill="#34c759",
|
|
)
|
|
if self.bg_canvas.winfo_exists()
|
|
else None
|
|
)
|
|
)
|
|
self.root.after(1000, lambda: self.show_page("init_scripts"))
|
|
|
|
threading.Thread(target=worker, daemon=True).start()
|