mirror of
https://github.com/dredx/prole.git
synced 2026-09-27 08:14:29 +00:00
Kong API Gateway (replacing prole nginx): - Add etc/init_kong.sh provisioning script (DB-less mode, prole-db namespace) - Add kong-deployment.yaml and kong-service.yaml manifests - Rewire ingress rules (svc/git/api.prole.org) to prole-db-kong:8000 - Update kustomization.yaml to reference kong manifests PostgREST & DB Manager in prole-db namespace: - Add etc/init_postgrest.sh and etc/init_db_manager.sh scripts - Add postgrest/db-manager deployment and service manifests - Add src/db-manager/ Node.js REST endpoint for backup triggers - Default NAMESPACE changed to prole-db in both scripts Docker image pre-load from PROLE_DATA/docker-import: - Add _preload_docker_images() to init_common_services.sh - Scan for .tar files exported by final_deployment.sh - Import via k3d image import (k3d) or ctr (k3s) before deployments - Increase rollout timeouts to 300s (configurable via ROLLOUT_TIMEOUT) in init_openbao.sh, init_opentofu.sh, init_garage_store.sh, init_registry.sh OpenTofu password resolution fix: - Add Kubernetes secret fallback in resolve_admin_password() - Change hard exit 1 to graceful return 1 with warning - Wrap call in if-guard so set -e doesn't abort the script chain Milestone fix (init scripts not running): - Add init_kong.sh, init_postgrest.sh, init_db_manager.sh to InitializationScriptsMilestone.execute() script list and arg branches - Previously only actions.py had these; milestones.py was missing them Installer integration: - Add Kong/PostgREST/DB Manager to silent installer _step_init_scripts - Add corresponding tabs and execution blocks in UI services.py
1204 lines
59 KiB
Python
1204 lines
59 KiB
Python
"""Initialization scripts, CNPG deployment and service-layer overlays."""
|
|
|
|
import os
|
|
import platform
|
|
import secrets
|
|
import shlex
|
|
import subprocess
|
|
import threading
|
|
import time
|
|
import webbrowser
|
|
from pathlib import Path
|
|
from datetime import datetime
|
|
import tkinter as tk
|
|
from tkinter import ttk, messagebox, filedialog
|
|
from installer.screen import TerminalConsole
|
|
from installer import screen as ui
|
|
from installer.core.env import (
|
|
PROJECT_ROOT,
|
|
_bool_str,
|
|
_deployment_mode_from_env,
|
|
_deployment_target_label,
|
|
_normalize_cluster_env,
|
|
)
|
|
|
|
|
|
class ServicesScreenMixin:
|
|
"""Initialization scripts, CNPG deployment and service-layer overlays."""
|
|
|
|
def _render_init_scripts_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, 'Prole', fill='#6e6e73', font=('SF Pro Text', 32, 'bold'), anchor='ne')
|
|
ui.canvas_text(self, right_margin, 85, "Infrastructure Automated.", fill='#6e6e73',
|
|
font=('SF Pro Text', 18), anchor='ne')
|
|
|
|
self._render_title('Initialization Scripts', y=150)
|
|
self._render_paragraph('Running initialization scripts to set up Garage, CloudNative-PG, and backups.', y=200)
|
|
|
|
y = 260
|
|
y = self._render_init_scripts_section(48, y, 900, 450, title="Execution Output")
|
|
|
|
# Use tk.Button
|
|
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(48, y + 20, window=self._init_scripts_button, anchor='nw', width=180)
|
|
self._canvas_items.append(btn_window)
|
|
self._overlay_widgets.append(self._init_scripts_button)
|
|
|
|
# Status Label
|
|
self._init_scripts_status_label = ui.canvas_text(self, 240, y + 32, "", fill='black', font=('SF Pro Text', 12))
|
|
self._canvas_items.append(self._init_scripts_status_label)
|
|
|
|
def _render_init_scripts_section(self, x: int, y: int, width: int, height: int, title: str | None = "Execution Output") -> int:
|
|
if title:
|
|
title_item = ui.canvas_text(self, x, y, title, fill='#1d1d1f', font=('SF Pro Text', 12, 'bold'))
|
|
self._canvas_items.append(title_item)
|
|
y += 30
|
|
|
|
# Use a background frame for the notebook to hide potential system borders
|
|
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_window = self.bg_canvas.create_window(x, y, window=notebook_bg, anchor='nw', width=width, height=height)
|
|
self._canvas_items.append(tab_window)
|
|
self._overlay_widgets.append(notebook_bg)
|
|
self._overlay_widgets.append(self.script_tabs)
|
|
|
|
self.script_consoles = {}
|
|
scripts = self._init_scripts_list()
|
|
self._script_tab_index = {}
|
|
|
|
for idx, (title, fname) in enumerate(scripts):
|
|
# Use a background frame to ensure NO borders are visible around the console
|
|
console_bg = tk.Frame(self.script_tabs, bg='white', highlightthickness=0, bd=0)
|
|
self.script_tabs.add(console_bg, text=title)
|
|
# Use TerminalConsole for consistent styling
|
|
if fname == 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[fname] = console
|
|
self._script_tab_index[fname] = idx
|
|
self._overlay_widgets.append(console_bg)
|
|
self._overlay_widgets.append(console)
|
|
|
|
self.script_tabs.bind("<<NotebookTabChanged>>", self._on_init_scripts_tab_changed)
|
|
self.safe_after(self._refresh_kubectl_status_tab, delay=50)
|
|
|
|
return y + height
|
|
|
|
def _init_scripts_list(self):
|
|
scripts = [
|
|
('Kubectl Status', self.KUBECTL_STATUS_TAB),
|
|
('Common Services', 'init_common_services.sh'),
|
|
('Common Services Log', 'common_services_log'),
|
|
('CloudNative-PG', 'init_cloudnative_pg.sh'),
|
|
]
|
|
if self.kerberos_enabled.get():
|
|
scripts.append(('Kerberos Realm', 'init_kerberos.sh'))
|
|
scripts.append(('Prole DB Backup', 'init_prole-db-backup.sh'))
|
|
scripts.append(('Kong API Gateway', 'init_kong.sh'))
|
|
scripts.append(('PostgREST', 'init_postgrest.sh'))
|
|
scripts.append(('DB Manager', 'init_db_manager.sh'))
|
|
scripts.append(('Monitoring', 'init_monitoring.sh'))
|
|
mode = self._deployment_mode()
|
|
if mode != "k3d":
|
|
scripts.append(('Nginx Ingress', 'init_nginx_ingress.sh'))
|
|
if mode != "k3s":
|
|
scripts.append(('Port Forwards', 'init_port_forwards.sh'))
|
|
return scripts
|
|
|
|
def run_init_scripts(self):
|
|
self._action_flags['init_scripts.run_scripts'] = True
|
|
def worker():
|
|
def _select_tab(script_name):
|
|
idx = getattr(self, '_script_tab_index', {}).get(script_name)
|
|
if idx is None:
|
|
return
|
|
self.safe_after(lambda: self.script_tabs.select(idx) if self.script_tabs.winfo_exists() else None)
|
|
|
|
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["PROLE_HOME"] = str(PROJECT_ROOT)
|
|
env["PROLE_SERVICE"] = str(PROJECT_ROOT)
|
|
env["PROLE_DB_USER"] = self.db_username.get()
|
|
env["DB_PASSWORD"] = password
|
|
env["GRAFANA_ADMIN_PASSWORD"] = password
|
|
env["NAMESPACE"] = (self.db_namespace.get() or '').strip()
|
|
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["PROLE_MODE"] = mode
|
|
env["DEPLOYMENT_MODE"] = mode
|
|
env["DEPLOYMENT_TARGET"] = _deployment_target_label(self.cluster_env.get())
|
|
mode_args = ["--mode", mode] if mode else []
|
|
|
|
logs_dir = self._resolve_prole_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 update
|
|
overall_success = True
|
|
if overall_success:
|
|
script = "init_common_services.sh"
|
|
_select_tab(script)
|
|
self.script_consoles[script].clear()
|
|
self.script_consoles[script].write(f"Running {script} update...\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)
|
|
self.script_consoles["common_services_log"].clear()
|
|
self.script_consoles["common_services_log"].write(f"Log file: {svc_log_path}\n\n")
|
|
self.script_consoles[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):
|
|
self.script_consoles[script].write(line)
|
|
self.script_consoles["common_services_log"].write(line)
|
|
self._process_script_output_line(line)
|
|
if svc_fp:
|
|
try:
|
|
svc_fp.write(line)
|
|
svc_fp.flush()
|
|
except Exception:
|
|
pass
|
|
|
|
svc_args = list(mode_args)
|
|
if self.kerberos_enabled.get():
|
|
svc_args.append("-k")
|
|
svc_args.append("update")
|
|
rc_svc = self.controller.run_script(
|
|
script,
|
|
args=svc_args,
|
|
env=env,
|
|
on_line=_svc_line
|
|
)
|
|
if svc_fp:
|
|
try:
|
|
svc_fp.close()
|
|
except Exception:
|
|
pass
|
|
|
|
if rc_svc != 0:
|
|
self.script_consoles[script].write(f"\nERROR: {script} update failed with code {rc_svc}\n")
|
|
overall_success = False
|
|
else:
|
|
self.script_consoles[script].write(f"\n{script} completed successfully.\n")
|
|
else:
|
|
self.script_consoles["init_common_services.sh"].write("Skipping common services initialization because previous steps failed.\n")
|
|
|
|
# 2. init_cloudnative_pg.sh initialize
|
|
if overall_success:
|
|
script = "init_cloudnative_pg.sh"
|
|
_select_tab(script)
|
|
self.script_consoles[script].clear()
|
|
self.script_consoles[script].write(f"Running {script} initialize...\n")
|
|
self.script_consoles[script].write("> bash etc/init_cloudnative_pg.sh 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 _cnpg_line(line):
|
|
self.script_consoles[script].write(line)
|
|
self._process_script_output_line(line)
|
|
if log_fp:
|
|
try:
|
|
log_fp.write(line)
|
|
log_fp.flush()
|
|
except Exception:
|
|
pass
|
|
|
|
rc2 = self.controller.run_script(
|
|
script,
|
|
args=mode_args + ['initialize'],
|
|
env=env,
|
|
on_line=_cnpg_line
|
|
)
|
|
if log_fp:
|
|
try:
|
|
log_fp.close()
|
|
except Exception:
|
|
pass
|
|
|
|
if rc2 != 0:
|
|
self.script_consoles[script].write(f"\nERROR: {script} initialize failed with code {rc2}\n")
|
|
overall_success = False
|
|
else:
|
|
self.script_consoles[script].write(f"\n{script} completed successfully.\n")
|
|
else:
|
|
if "init_cloudnative_pg.sh" in self.script_consoles:
|
|
self.script_consoles["init_cloudnative_pg.sh"].write("Skipping CloudNative-PG initialization because previous steps failed.\n")
|
|
|
|
# 3. init_kerberos.sh initialize (optional)
|
|
if overall_success and self.kerberos_enabled.get():
|
|
script = "init_kerberos.sh"
|
|
if script in self.script_consoles:
|
|
_select_tab(script)
|
|
self.script_consoles[script].clear()
|
|
self.script_consoles[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 _krb_line(line):
|
|
self.script_consoles[script].write(line)
|
|
self._process_script_output_line(line)
|
|
if log_fp:
|
|
try:
|
|
log_fp.write(line)
|
|
log_fp.flush()
|
|
except Exception:
|
|
pass
|
|
|
|
rc_krb = self.controller.run_script(
|
|
script,
|
|
args=mode_args + ['initialize'],
|
|
env=env,
|
|
on_line=_krb_line
|
|
)
|
|
if log_fp:
|
|
try:
|
|
log_fp.close()
|
|
except Exception:
|
|
pass
|
|
|
|
if rc_krb != 0:
|
|
self.script_consoles[script].write(f"\nERROR: {script} initialize failed with code {rc_krb}\n")
|
|
overall_success = False
|
|
else:
|
|
self.script_consoles[script].write(f"\n{script} completed successfully.\n")
|
|
else:
|
|
fallback_console = self.script_consoles.get("init_cloudnative_pg.sh")
|
|
if fallback_console:
|
|
fallback_console.write("Kerberos init console missing; skipping.\n")
|
|
elif not self.kerberos_enabled.get():
|
|
if "init_kerberos.sh" in self.script_consoles:
|
|
self.script_consoles["init_kerberos.sh"].write("Kerberos auth disabled; skipping init_kerberos.sh.\n")
|
|
# 6. init_prole-db-backup.sh start
|
|
if overall_success:
|
|
script = "init_prole-db-backup.sh"
|
|
_select_tab(script)
|
|
self.script_consoles[script].clear()
|
|
self.script_consoles[script].write(f"Running {script} start...\n")
|
|
|
|
log_path = _log_path_for(script)
|
|
self._record_install_log(log_path)
|
|
try:
|
|
log_fp = log_path.open('a', encoding='utf-8')
|
|
except Exception:
|
|
log_fp = None
|
|
|
|
def _backup_line(line):
|
|
self.script_consoles[script].write(line)
|
|
self._process_script_output_line(line)
|
|
if log_fp:
|
|
try:
|
|
log_fp.write(line)
|
|
log_fp.flush()
|
|
except Exception:
|
|
pass
|
|
|
|
rc_backup = self.controller.run_script(
|
|
script,
|
|
args=mode_args + ['start'],
|
|
env=env,
|
|
on_line=_backup_line
|
|
)
|
|
if log_fp:
|
|
try:
|
|
log_fp.close()
|
|
except Exception:
|
|
pass
|
|
|
|
if rc_backup != 0:
|
|
self.script_consoles[script].write(f"\nERROR: {script} start failed with code {rc_backup}\n")
|
|
overall_success = False
|
|
else:
|
|
self.script_consoles[script].write(f"\n{script} completed successfully.\n")
|
|
else:
|
|
self.script_consoles["init_prole-db-backup.sh"].write("Skipping Prole DB backup because previous steps failed.\n")
|
|
|
|
# 7. init_kong.sh start
|
|
if overall_success:
|
|
script = "init_kong.sh"
|
|
if script in self.script_consoles:
|
|
_select_tab(script)
|
|
self.script_consoles[script].clear()
|
|
self.script_consoles[script].write(f"Running {script} start...\n")
|
|
|
|
log_path = _log_path_for(script)
|
|
self._record_install_log(log_path)
|
|
try:
|
|
log_fp = log_path.open('a', encoding='utf-8')
|
|
except Exception:
|
|
log_fp = None
|
|
|
|
def _kong_line(line):
|
|
self.script_consoles[script].write(line)
|
|
self._process_script_output_line(line)
|
|
if log_fp:
|
|
try:
|
|
log_fp.write(line)
|
|
log_fp.flush()
|
|
except Exception:
|
|
pass
|
|
|
|
rc_kong = self.controller.run_script(
|
|
script,
|
|
args=mode_args + ['start'],
|
|
env=env,
|
|
on_line=_kong_line
|
|
)
|
|
if log_fp:
|
|
try:
|
|
log_fp.close()
|
|
except Exception:
|
|
pass
|
|
|
|
if rc_kong != 0:
|
|
self.script_consoles[script].write(f"\nERROR: {script} start failed with code {rc_kong}\n")
|
|
overall_success = False
|
|
else:
|
|
self.script_consoles[script].write(f"\n{script} completed successfully.\n")
|
|
else:
|
|
if "init_kong.sh" in self.script_consoles:
|
|
self.script_consoles["init_kong.sh"].write("Skipping Kong because previous steps failed.\n")
|
|
|
|
# 7b. init_postgrest.sh start
|
|
if overall_success:
|
|
script = "init_postgrest.sh"
|
|
if script in self.script_consoles:
|
|
_select_tab(script)
|
|
self.script_consoles[script].clear()
|
|
self.script_consoles[script].write(f"Running {script} start...\n")
|
|
|
|
log_path = _log_path_for(script)
|
|
self._record_install_log(log_path)
|
|
try:
|
|
log_fp = log_path.open('a', encoding='utf-8')
|
|
except Exception:
|
|
log_fp = None
|
|
|
|
def _postgrest_line(line):
|
|
self.script_consoles[script].write(line)
|
|
self._process_script_output_line(line)
|
|
if log_fp:
|
|
try:
|
|
log_fp.write(line)
|
|
log_fp.flush()
|
|
except Exception:
|
|
pass
|
|
|
|
rc_postgrest = self.controller.run_script(
|
|
script,
|
|
args=mode_args + ['start'],
|
|
env=env,
|
|
on_line=_postgrest_line
|
|
)
|
|
if log_fp:
|
|
try:
|
|
log_fp.close()
|
|
except Exception:
|
|
pass
|
|
|
|
if rc_postgrest != 0:
|
|
self.script_consoles[script].write(f"\nERROR: {script} start failed with code {rc_postgrest}\n")
|
|
overall_success = False
|
|
else:
|
|
self.script_consoles[script].write(f"\n{script} completed successfully.\n")
|
|
else:
|
|
if "init_postgrest.sh" in self.script_consoles:
|
|
self.script_consoles["init_postgrest.sh"].write("Skipping PostgREST because previous steps failed.\n")
|
|
|
|
# 7c. init_db_manager.sh start
|
|
if overall_success:
|
|
script = "init_db_manager.sh"
|
|
if script in self.script_consoles:
|
|
_select_tab(script)
|
|
self.script_consoles[script].clear()
|
|
self.script_consoles[script].write(f"Running {script} start...\n")
|
|
|
|
log_path = _log_path_for(script)
|
|
self._record_install_log(log_path)
|
|
try:
|
|
log_fp = log_path.open('a', encoding='utf-8')
|
|
except Exception:
|
|
log_fp = None
|
|
|
|
def _dbmgr_line(line):
|
|
self.script_consoles[script].write(line)
|
|
self._process_script_output_line(line)
|
|
if log_fp:
|
|
try:
|
|
log_fp.write(line)
|
|
log_fp.flush()
|
|
except Exception:
|
|
pass
|
|
|
|
rc_dbmgr = self.controller.run_script(
|
|
script,
|
|
args=mode_args + ['start'],
|
|
env=env,
|
|
on_line=_dbmgr_line
|
|
)
|
|
if log_fp:
|
|
try:
|
|
log_fp.close()
|
|
except Exception:
|
|
pass
|
|
|
|
if rc_dbmgr != 0:
|
|
self.script_consoles[script].write(f"\nERROR: {script} start failed with code {rc_dbmgr}\n")
|
|
overall_success = False
|
|
else:
|
|
self.script_consoles[script].write(f"\n{script} completed successfully.\n")
|
|
else:
|
|
if "init_db_manager.sh" in self.script_consoles:
|
|
self.script_consoles["init_db_manager.sh"].write("Skipping DB Manager because previous steps failed.\n")
|
|
|
|
# 8. init_monitoring.sh initialize
|
|
if overall_success:
|
|
script = "init_monitoring.sh"
|
|
_select_tab(script)
|
|
self.script_consoles[script].clear()
|
|
self.script_consoles[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):
|
|
self.script_consoles[script].write(line)
|
|
self._process_script_output_line(line)
|
|
if log_fp:
|
|
try:
|
|
log_fp.write(line)
|
|
log_fp.flush()
|
|
except Exception:
|
|
pass
|
|
# Extract Grafana password if present
|
|
if "GRAFANA_ADMIN_PASSWORD=" in line:
|
|
pwd = line.split("GRAFANA_ADMIN_PASSWORD=")[1].strip()
|
|
if pwd:
|
|
self.prole_cfg_data['Monitoring'] = {'GRAFANA_ADMIN_PASSWORD': pwd}
|
|
self._save_prole_cfg()
|
|
|
|
rc_mon = self.controller.run_script(
|
|
script,
|
|
args=mode_args + ['initialize'],
|
|
env=env,
|
|
on_line=_monitoring_line
|
|
)
|
|
if log_fp:
|
|
try:
|
|
log_fp.close()
|
|
except Exception:
|
|
pass
|
|
|
|
if rc_mon != 0:
|
|
self.script_consoles[script].write(f"\nERROR: {script} initialize failed with code {rc_mon}\n")
|
|
overall_success = False
|
|
else:
|
|
self.script_consoles[script].write(f"\n{script} completed successfully.\n")
|
|
else:
|
|
if "init_monitoring.sh" in self.script_consoles:
|
|
self.script_consoles["init_monitoring.sh"].write("Skipping Monitoring initialization because previous steps failed.\n")
|
|
|
|
# 9. 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)
|
|
self.script_consoles[script].clear()
|
|
self.script_consoles[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):
|
|
self.script_consoles[script].write(line)
|
|
self._process_script_output_line(line)
|
|
if log_fp:
|
|
try:
|
|
log_fp.write(line)
|
|
log_fp.flush()
|
|
except Exception:
|
|
pass
|
|
|
|
rc_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:
|
|
self.script_consoles[script].write(f"\nERROR: {script} initialize failed with code {rc_ing}\n")
|
|
overall_success = False
|
|
else:
|
|
self.script_consoles[script].write(f"\n{script} completed successfully.\n")
|
|
elif mode != "k3d":
|
|
self.script_consoles["init_nginx_ingress.sh"].write("Skipping Nginx Ingress because previous steps failed.\n")
|
|
|
|
# 10. init_port_forwards.sh start (k3d only — not used in k3s)
|
|
if overall_success and mode != "k3s":
|
|
script = "init_port_forwards.sh"
|
|
if script in self.script_consoles:
|
|
_select_tab(script)
|
|
self.script_consoles[script].clear()
|
|
self.script_consoles[script].write(f"Running {script} start...\n")
|
|
|
|
def _pf_line(line):
|
|
self.script_consoles[script].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=["--mode", mode, "start"],
|
|
env=env_pf,
|
|
on_line=_pf_line
|
|
)
|
|
if rc_pf != 0:
|
|
self.script_consoles[script].write(f"\nERROR: {script} start failed with code {rc_pf}\n")
|
|
overall_success = False
|
|
else:
|
|
running = False
|
|
for _ in range(10):
|
|
status_lines = []
|
|
|
|
def _status_line(line):
|
|
status_lines.append(line)
|
|
self.script_consoles[script].write(line)
|
|
|
|
rc_status = self.controller.run_script(
|
|
script,
|
|
args=["--mode", mode, "status", "grafana"],
|
|
env=env_pf,
|
|
on_line=_status_line
|
|
)
|
|
running = any(l.lstrip().startswith("RUNNING") and "grafana" in l for l in status_lines)
|
|
if rc_status == 0 and running:
|
|
break
|
|
time.sleep(1)
|
|
|
|
if not running:
|
|
self.script_consoles[script].write("\nERROR: Grafana port-forward not running after start.\n")
|
|
overall_success = False
|
|
else:
|
|
self.script_consoles[script].write("\nPort-forwards running (grafana ok).\n")
|
|
|
|
# Verify critical secrets
|
|
ns = env.get("NAMESPACE", "default")
|
|
self.script_consoles["init_cloudnative_pg.sh"].write(f"\nVerifying critical secrets in namespace {ns}...\n")
|
|
critical_secrets = ["prole-db-user", "prole-db-superuser", "cnpg-admin-key"]
|
|
missing_secrets = []
|
|
for secret in critical_secrets:
|
|
rc_s, _ = self._run_cmd_capture(["kubectl", "get", "secret", secret, "-n", ns])
|
|
if rc_s != 0:
|
|
missing_secrets.append(secret)
|
|
|
|
if missing_secrets:
|
|
self.script_consoles["init_cloudnative_pg.sh"].write(f"CRITICAL: Missing secrets in namespace '{ns}': {', '.join(missing_secrets)}.\n")
|
|
self.script_consoles["init_cloudnative_pg.sh"].write("Database initialization will fail without these secrets.\n")
|
|
self.safe_after(lambda: self.bg_canvas.itemconfig(self._init_scripts_status_label, text="Critical secret missing.", fill='#ff3b30') if self.bg_canvas.winfo_exists() else None)
|
|
overall_success = False
|
|
|
|
if overall_success:
|
|
self.safe_after(lambda: self.bg_canvas.itemconfig(self._init_scripts_status_label, text="Initialization complete!", fill='#34c759') 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.prole_cfg_data['Initialization Scripts']['STATUS'] = 'Completed' if self._scripts_success else 'Attempted'
|
|
self.safe_after(self._save_prole_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("PROLE_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 _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, 'Prole', fill='#6e6e73', font=('SF Pro Text', 32, 'bold'), anchor='ne')
|
|
ui.canvas_text(self, right_margin, 85, "Infrastructure Automated.", 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 = ['prole-dev-cluster', 'prole-service-cluster', 'prole-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["PROLE_HOME"] = str(PROJECT_ROOT)
|
|
env["PROLE_SERVICE"] = str(PROJECT_ROOT)
|
|
env["NAMESPACE"] = (self.db_namespace.get() or '').strip()
|
|
env["NAMESPACE"] = (self.db_namespace.get() or '').strip()
|
|
if mode:
|
|
env["PROLE_MODE"] = mode
|
|
env["DEPLOYMENT_MODE"] = mode
|
|
env["DEPLOYMENT_TARGET"] = _deployment_target_label(self.cluster_env.get())
|
|
|
|
self._cnpg_deploy_console.clear()
|
|
self._cnpg_deploy_console.write("Starting CloudNative-PG deployment...\n")
|
|
self._cnpg_deploy_console.write(f"> bash etc/init_cloudnative_pg.sh --mode {mode or 'k3d'} deploy latest\n\n")
|
|
|
|
cmd = ['bash', str(etc_dir / 'init_cloudnative_pg.sh')]
|
|
if mode:
|
|
cmd += ['--mode', mode]
|
|
cmd += ['deploy', 'latest']
|
|
proc = subprocess.Popen(cmd,
|
|
stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, env=env)
|
|
|
|
while True:
|
|
line = proc.stdout.readline()
|
|
if not line and proc.poll() is not None:
|
|
break
|
|
if line:
|
|
self._cnpg_deploy_console.write(line)
|
|
|
|
if proc.returncode == 0:
|
|
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
|
|
else:
|
|
self.safe_after(lambda: self.bg_canvas.itemconfig(self._cnpg_deploy_status_label, text=f"Deployment failed (code {proc.returncode})", 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)
|
|
|
|
etc_dir = PROJECT_ROOT / "etc"
|
|
# Prepare environment for scripts
|
|
env = os.environ.copy()
|
|
env["PROLE_HOME"] = str(PROJECT_ROOT)
|
|
env["PROLE_SERVICE"] = str(PROJECT_ROOT)
|
|
mode = self._deployment_mode()
|
|
if mode:
|
|
env["PROLE_MODE"] = mode
|
|
env["DEPLOYMENT_MODE"] = mode
|
|
env["DEPLOYMENT_TARGET"] = _deployment_target_label(self.cluster_env.get())
|
|
|
|
self._cnpg_deploy_console.clear()
|
|
self._cnpg_deploy_console.write("Starting manual recreate rollout for prole-db cluster...\n")
|
|
self._cnpg_deploy_console.write(f"> bash etc/init_cloudnative_pg.sh --mode {mode or 'k3d'} rollout\n\n")
|
|
|
|
cmd = ['bash', str(etc_dir / 'init_cloudnative_pg.sh')]
|
|
if mode:
|
|
cmd += ['--mode', mode]
|
|
cmd += ['rollout']
|
|
proc = subprocess.Popen(cmd,
|
|
stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, env=env)
|
|
|
|
while True:
|
|
line = proc.stdout.readline()
|
|
if not line and proc.poll() is not None:
|
|
break
|
|
if line:
|
|
self._cnpg_deploy_console.write(line)
|
|
|
|
if proc.returncode == 0:
|
|
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)
|
|
else:
|
|
self._cnpg_deploy_console.write(f"\nRollout failed with code {proc.returncode}\n")
|
|
self.safe_after(lambda: self.bg_canvas.itemconfig(self._cnpg_deploy_status_label, text=f"Rollout failed (code {proc.returncode})", 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_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, 'Prole', fill='#6e6e73', font=('SF Pro Text', 32, 'bold'), anchor='ne')
|
|
ui.canvas_text(self, right_margin, 85, "Infrastructure Automated.", 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)
|
|
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, 'Prole', fill='#6e6e73', font=('SF Pro Text', 32, 'bold'), anchor='ne')
|
|
ui.canvas_text(self, right_margin, 85, "Infrastructure Automated.", 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 worker():
|
|
key_path = Path.home() / ".ssh" / "id_prole_ed25519"
|
|
key_path.parent.mkdir(parents=True, exist_ok=True)
|
|
password = self.db_password.get()
|
|
namespace = (self.db_namespace.get() or '').strip()
|
|
|
|
# Pre-pull dependent images into local registry before initialization
|
|
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)
|
|
# In case of error, we might want to let the user see it before continuing or stopping
|
|
time.sleep(2)
|
|
self.root.after(500, lambda: self.show_page('init_password'))
|
|
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)
|
|
env["PROLE_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 = self.controller.run_script(
|
|
"init_openbao.sh",
|
|
args=["initialize"],
|
|
env=env,
|
|
stdin_text=f"{password}\n",
|
|
on_line=lambda l: console.write(l)
|
|
)
|
|
if rc != 0:
|
|
console.write(f"\nOpenBao initialization failed (code {rc}).\n")
|
|
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)
|
|
self.root.after(500, lambda: self.show_page('init_password'))
|
|
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_db_build'))
|
|
|
|
threading.Thread(target=worker, daemon=True).start()
|