prole/installer/ui/screens/supabase.py
chrisfu ec50f82cc8 refactor: deduplicate installer business logic & split screens.py into package
Separation of concerns: merge silent/UI actions & modularize screens.

ProleInstallerBase (actions.py): Created shared base class with 37 deduplicated methods previously duplicated between ProleSilentInstaller and ProleInstaller. Namespace, environment, secret, deployment, port-forward, authority/repair, image, and logging helpers now defined once. Subclasses override _get_input() to bridge their data-access layers.

screens.py -> screens/ package (18 mixin modules): Split 10,234-line monolithic screens.py into focused mixin modules: base, navigation, welcome, dependencies, network, environment, database, cluster, services, security, ollama, supabase, docker, build, packaging, deploy, validate, cfg. __init__.py composes ProleInstaller from all mixins and re-exports has_display(), main() for full backward compatibility.

All 37 tests pass with no regressions.
2026-02-20 14:43:20 -08:00

213 lines
9.4 KiB
Python

"""Supabase configuration and deployment screen."""
import os
import subprocess
import threading
import tkinter as tk
from installer import screen as ui
from installer.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('Prole::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)
y = 280
self._supabase_status_var = tk.StringVar(value="Ready")
status_item = ui.canvas_text(self, 48, y, "Status: Ready", 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=320, title="Deployment Output", width=880, height=380)
y = 740
# 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
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
self._supabase_console.write("Starting Supabase deployment...\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 = PROJECT_ROOT / "conf" / "prole.cfg"
if candidate.exists():
cfg_path = candidate
except Exception:
cfg_path = None
if cfg_path:
args.extend(["-c", str(cfg_path)])
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()