prole/knoe/ui/screens/supabase.py
chrisfu c0a7d0c5dc Refine installer orchestration and Supabase rendering paths
- improve installer/action/controller flow and shell-variable expansion handling across screens\n- adjust Supabase Helm rendering and storage deployment templates\n- align monitoring, cloudnative-pg and repair pipeline behavior with updated config paths\n- refresh and expand installer/core regression tests around milestones, navigation and repair logic

Co-authored-by: Junie <junie@jetbrains.com>
2026-03-31 23:15:54 -07:00

422 lines
15 KiB
Python

"""Supabase configuration and deployment screen."""
import os
import subprocess
import threading
import tkinter as tk
from knoe import prole_conf
from knoe import screen as ui
from knoe.core.env import PROJECT_ROOT, _normalize_cluster_env, _parse_bool
class SupabaseScreenMixin:
"""Supabase configuration and deployment screen."""
def _render_supabase_config_page(self):
# Letterhead at top right (matching welcome screen theme)
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("Supabase", y=150)
description = (
"Launches the Supabase open-source stack via supabase/deploy.sh.\n"
"Default mode is local (Docker Compose). Set SUPABASE_DEPLOY_MODE=k3d\n"
"to deploy into the 'supabase' Kubernetes namespace."
)
self._render_paragraph(description, y=210)
enable_cb = tk.Checkbutton(
self.bg_canvas,
text="Enable Supabase",
variable=self.supabase_enabled,
command=self._on_supabase_toggle,
bg="white",
fg="black",
activebackground="white",
selectcolor="white",
font=("SF Pro Text", 11),
)
enable_window = self.bg_canvas.create_window(
48, 250, window=enable_cb, anchor="nw"
)
self._canvas_items.append(enable_window)
self._overlay_widgets.append(enable_cb)
y = 280
node_label = ui.canvas_text(
self,
48,
y,
"Node selector:",
fill="black",
font=("SF Pro Text", 12),
)
self._canvas_items.append(node_label)
node_entry = tk.Entry(
self.bg_canvas,
textvariable=self.supabase_pv_node,
font=("SF Pro Text", 11),
width=38,
relief="solid",
bd=1,
)
node_entry_window = self.bg_canvas.create_window(
190, y - 2, window=node_entry, anchor="nw"
)
self._canvas_items.append(node_entry_window)
self._overlay_widgets.append(node_entry)
y += 36
storage_label = ui.canvas_text(
self,
48,
y,
"Storage directory:",
fill="black",
font=("SF Pro Text", 12),
)
self._canvas_items.append(storage_label)
storage_entry = tk.Entry(
self.bg_canvas,
textvariable=self.supabase_pv_base_dir,
font=("SF Pro Text", 11),
width=38,
relief="solid",
bd=1,
)
storage_entry_window = self.bg_canvas.create_window(
190, y - 2, window=storage_entry, anchor="nw"
)
self._canvas_items.append(storage_entry_window)
self._overlay_widgets.append(storage_entry)
y += 42
initial_status = "Ready" if self.supabase_enabled.get() else "Disabled"
self._supabase_status_var = tk.StringVar(value=initial_status)
status_item = ui.canvas_text(
self,
48,
y,
f"Status: {initial_status}",
fill="black",
font=("SF Pro Text", 12),
)
def update_status_text(*args):
try:
self.bg_canvas.itemconfig(
status_item, text=f"Status: {self._supabase_status_var.get()}"
)
except Exception:
pass
self._supabase_status_var.trace_add("write", update_status_text)
# Standardized Console Output
self._supabase_console = self._create_console_output(
y=390, title="Deployment Output", width=880, height=310
)
y = 720
# Deploy Supabase Button
self._supabase_deploy_button = tk.Button(
self.bg_canvas,
text="Deploy Supabase",
command=self.run_supabase_deploy,
bg="#F5F5DC",
fg="black",
activebackground="#E5E5D5",
activeforeground="black",
highlightbackground="#F5F5DC",
highlightcolor="#F5F5DC",
highlightthickness=0,
relief="flat",
bd=0,
cursor="hand2",
disabledforeground="#8B8B7A",
font=("SF Pro Text", 11),
padx=20,
pady=10,
)
btn_window = self.bg_canvas.create_window(
48, y, window=self._supabase_deploy_button, anchor="nw"
)
self._overlay_widgets.append(self._supabase_deploy_button)
self._canvas_items.append(btn_window)
def run_supabase_deploy(self):
if getattr(self, "_supabase_deploying", False):
return
if not self.supabase_enabled.get():
self._supabase_status_var.set("Disabled")
self.update_footer()
return
self._supabase_deploying = True
self._supabase_deploy_button.configure(state="disabled")
self._supabase_status_var.set("Deploying...")
self._supabase_success = False
self._supabase_console.clear()
self.update_footer()
def worker():
namespace = (
(self.db_namespace.get() or "").strip()
or os.environ.get("NAMESPACE")
or "default"
)
env = os.environ.copy()
env["PROLE_HOME"] = str(PROJECT_ROOT)
env["PROLE_SERVICE"] = str(PROJECT_ROOT)
env["NAMESPACE"] = namespace
pv_node = ""
try:
pv_node = (self.supabase_pv_node.get() or "").strip()
except Exception:
pv_node = ""
if pv_node:
env["SUPABASE_PV_NODE"] = pv_node
pv_base_dir = ""
try:
pv_base_dir = (self.supabase_pv_base_dir.get() or "").strip()
except Exception:
pv_base_dir = ""
if pv_base_dir:
env["SUPABASE_PV_BASE_DIR"] = pv_base_dir
env["SUPABASE_PV_BASE"] = pv_base_dir
self._supabase_console.write("Starting Supabase deployment...\n")
self._supabase_console.write(
f"Placement: node={pv_node or 'auto'} storage={pv_base_dir or 'auto'}\n"
)
script_path = PROJECT_ROOT / "supabase" / "deploy.sh"
if not script_path.exists():
self._supabase_console.write(f"Error: {script_path} not found.\n")
self.safe_after(
lambda: self._supabase_status_var.set("Failed (Script not found)")
)
self.safe_after(
lambda: self._supabase_deploy_button.configure(state="normal")
)
self._supabase_deploying = False
return
mode = (
os.environ.get("SUPABASE_DEPLOY_MODE")
or os.environ.get("SUPABASE_MODE")
or ""
).strip()
if not mode:
# Match cluster environment to supabase deploy mode
cluster_env = _normalize_cluster_env(self.cluster_env.get())
if cluster_env == "dev":
mode = "k3d"
elif cluster_env in ("service", "prod"):
mode = "k8s"
else:
mode = "k3d"
args = ["--mode", mode]
cfg_path = None
try:
if self._cfg_path_override and self._cfg_path_override.exists():
cfg_path = self._cfg_path_override
else:
conf_dir = self._resolve_prole_conf_dir()
candidate = conf_dir / "prole.cfg"
if candidate.exists():
cfg_path = candidate
if not cfg_path:
candidate = prole_conf.entrypoint_path(
prole_conf.resolve_prole_conf_dir(PROJECT_ROOT)
)
if candidate.exists():
cfg_path = candidate
except Exception:
cfg_path = None
if cfg_path:
args.extend(["-c", str(cfg_path)])
try:
self._save_prole_cfg()
except Exception:
pass
if _parse_bool(os.environ.get("SUPABASE_USE_DEV_COMPOSE"), False):
args.append("--with-dev-helpers")
if _parse_bool(os.environ.get("SUPABASE_FOREGROUND"), False):
args.append("--foreground")
self._supabase_console.write(f"Mode: {mode}\n")
cmd = ["bash", str(script_path)] + args
try:
proc = subprocess.Popen(
cmd,
cwd=str(PROJECT_ROOT),
env=env,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True,
bufsize=1,
)
except Exception as e:
self._supabase_console.write(f"Failed to start deploy: {e}\n")
self.safe_after(
lambda: self._supabase_status_var.set("Failed (Launch error)")
)
self.safe_after(
lambda: self._supabase_deploy_button.configure(state="normal")
)
self._supabase_deploying = False
return
if proc.stdout:
for line in iter(proc.stdout.readline, ""):
if line:
self._supabase_console.write(line)
proc.stdout.close()
rc = proc.wait()
if rc == 0:
self._supabase_console.write(
"\nSupabase deployment completed successfully.\n"
)
ports_script = PROJECT_ROOT / "etc" / "init_supabase_ports.sh"
self.safe_after(
lambda: self._supabase_status_var.set("Configuring Ports...")
)
if not ports_script.exists():
self._supabase_success = False
self.safe_after(
lambda: self._supabase_status_var.set(
"Failed (Ports script not found)"
)
)
self._supabase_console.write(f"Error: {ports_script} not found.\n")
else:
self._supabase_console.write(
f"\nConfiguring Supabase ports for namespace '{namespace}'...\n"
)
self._supabase_console.write(
f"> bash etc/init_supabase_ports.sh -n {namespace}\n\n"
)
try:
ports_proc = subprocess.Popen(
["bash", str(ports_script), "-n", namespace],
cwd=str(PROJECT_ROOT),
env=env,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True,
bufsize=1,
)
except Exception as e:
self._supabase_success = False
self.safe_after(
lambda: self._supabase_status_var.set(
"Failed (Ports launch error)"
)
)
self._supabase_console.write(
f"Failed to start port wiring: {e}\n"
)
else:
if ports_proc.stdout:
for line in iter(ports_proc.stdout.readline, ""):
if line:
self._supabase_console.write(line)
ports_proc.stdout.close()
ports_rc = ports_proc.wait()
if ports_rc == 0:
self._supabase_success = True
self.safe_after(
lambda: self._supabase_status_var.set(
"Deployed Successfully"
)
)
self._supabase_console.write(
"\nSupabase port wiring completed successfully.\n"
)
else:
self._supabase_success = False
self.safe_after(
lambda: self._supabase_status_var.set(
f"Failed (Ports code {ports_rc})"
)
)
self._supabase_console.write(
f"\nSupabase port wiring failed with exit code {ports_rc}.\n"
)
else:
self._supabase_success = False
self.safe_after(
lambda: self._supabase_status_var.set(f"Failed (Code {rc})")
)
self._supabase_console.write(
f"\nSupabase deployment failed with exit code {rc}.\n"
)
self._supabase_deploying = False
self.safe_after(
lambda: self._supabase_deploy_button.configure(state="normal")
)
self.safe_after(self.update_footer)
threading.Thread(target=worker, daemon=True).start()
def _on_supabase_toggle(self):
enabled = self.supabase_enabled.get()
try:
self.prole_cfg_data["Optional Features"]["SUPABASE_ENABLED"] = str(enabled)
self._save_prole_cfg()
except Exception:
pass
if not enabled:
self._supabase_success = False
if hasattr(self, "_supabase_status_var"):
self._supabase_status_var.set("Disabled")
try:
self._supabase_console.write("Supabase disabled. Deployment skipped.\n")
except Exception:
pass
self.update_footer()
return
if hasattr(self, "_supabase_status_var"):
self._supabase_status_var.set("Enabled")
try:
self._supabase_console.clear()
self._supabase_console.write("Supabase enabled. Starting deployment...\n")
except Exception:
pass
self.update_footer()
self.safe_after(self.run_supabase_deploy)