mirror of
https://github.com/dredx/prole.git
synced 2026-09-23 10:13:58 +00:00
Summary: Removed the prole-db-manager microservice and simplified deployment to use prole-authority as the internal management and authorization point. Fixed two blocking bugs that prevented silent install from completing on knoe-dev-cluster. Removed: prole-db-manager - Deleted db-manager-deployment.yaml and db-manager-service.yaml from opentofu manifests - Deleted src/db-manager/ (Dockerfile, server.js, package.json, tests) - Removed prole-db-manager port-forward mapping from installer/core/env.py - Removed init_db_manager.sh from Initialization Scripts (milestones.py, actions.py) - Removed init_certmgr.sh and init_db_manager.sh tabs from services screen (services.py) - Removed live k8s Deployment/Service from knoe-dev-cluster Fixed: PostgreSQL version downgrade error (pg17 -> pg18) - Created conf/postgresql/.version with value 18 - Updated k8s/prole/prole-db.yaml and prole-db-recovery.yaml.tpl imageName to prole-db:18-089 - Fixed _init_database_options_state() to restore saved version_type from prole.cfg so db_version_type defaults to v18 (pg18) instead of silently reverting to pg17 - Added database_options.* keys to _collect_input_snapshot() in cfg.py so distribution, version_type, and all extension toggles persist to prole.cfg Fixed: Cluster name inconsistency - Removed stale prole-dev-cluster references; all scripts now use knoe-dev-cluster - Added knoe-dev-cluster to mode-detection case in etc/prole_cfg.sh Config: conf/prole.cfg - Set kerberos_config.enabled = False, KERBEROS_AUTO_ENABLED = False - Added database_options.distribution = percona, version_type = v18 - Added all 13 extension flags set to True (postgis, pgvector, pgcrypto, pgaudit, pg_repack, pg_stat_statements, pg_buffercache, pg_freespacemap, pgrowlocks, postgres_fdw, dblink, pg_stat_monitor, pgbadger) Verification: ./install.py -s -l -v -c conf/prole.cfg completed successfully. CNPG deployed prole-db:18-089 to knoe-dev-cluster; all milestones passed. Co-authored-by: Junie <junie@jetbrains.com>
992 lines
35 KiB
Python
992 lines
35 KiB
Python
"""Deployment screen, final deployment pipeline and LaunchAgent setup."""
|
|
|
|
import os
|
|
import plistlib
|
|
import secrets
|
|
import subprocess
|
|
import threading
|
|
from pathlib import Path
|
|
import tkinter as tk
|
|
from tkinter import ttk, messagebox, filedialog
|
|
from installer import screen as ui
|
|
from installer.core.env import PROJECT_ROOT
|
|
|
|
|
|
class DeployScreenMixin:
|
|
"""Deployment screen, final deployment pipeline and LaunchAgent setup."""
|
|
|
|
def _render_deploy_page(self):
|
|
self._render_title("Deploy", y=40)
|
|
self._render_paragraph(
|
|
"Preparing to deploy. We will verify steps and perform actions as needed.",
|
|
y=90,
|
|
)
|
|
# Render simple list of steps (static view). Runtime updates can redraw as needed.
|
|
y = 140
|
|
left = 56
|
|
if not hasattr(self, "deploy_steps"):
|
|
self.deploy_steps = [
|
|
{"name": "Build Prole macOS app", "status": "pending"},
|
|
{"name": "Check Docker is running", "status": "pending"},
|
|
{"name": "Install Prole Tools.app", "status": "pending"},
|
|
]
|
|
for step in self.deploy_steps:
|
|
# status circle (pending empty)
|
|
self._canvas_items.append(
|
|
ui.canvas_oval(self, left, y, left + 18, y + 18, outline="#b0b0b0")
|
|
)
|
|
self._canvas_items.append(
|
|
ui.canvas_text(
|
|
self,
|
|
left + 26,
|
|
y - 2,
|
|
step["name"],
|
|
fill="#1d1d1f",
|
|
font=("Helvetica", 12),
|
|
)
|
|
)
|
|
y += 26
|
|
|
|
def on_deploy(self):
|
|
if getattr(self, "_deploy_running", False):
|
|
return
|
|
self._deploy_running = True
|
|
try:
|
|
if self.deploy_button:
|
|
self.deploy_button.configure(state="disabled")
|
|
except Exception:
|
|
pass
|
|
|
|
def worker():
|
|
err = None
|
|
try:
|
|
self.ensure_prole_env()
|
|
self.reload_env_from_shell()
|
|
except Exception as e:
|
|
err = f"Failed to load environment: {e}"
|
|
try:
|
|
self._ensure_prole_directories()
|
|
if "Install" in self.prole_cfg_data:
|
|
self.prole_cfg_data["Install"]["STATUS"] = "Deployed"
|
|
self._save_prole_cfg()
|
|
except Exception as e:
|
|
err = err or f"Failed to prepare Prole directories: {e}"
|
|
|
|
if err:
|
|
self.safe_after(lambda: messagebox.showerror("Deploy", err))
|
|
else:
|
|
self.safe_after(self.open_drag_install_window)
|
|
|
|
def _finish():
|
|
self._deploy_running = False
|
|
try:
|
|
if self.deploy_button:
|
|
self.deploy_button.configure(state="normal")
|
|
except Exception:
|
|
pass
|
|
|
|
self.safe_after(_finish)
|
|
|
|
threading.Thread(target=worker, daemon=True).start()
|
|
|
|
def on_launch(self):
|
|
app_path = self._get_prole_dist_dir() / "Prole Tools.app"
|
|
if app_path.exists():
|
|
try:
|
|
subprocess.Popen(["open", str(app_path)])
|
|
except Exception:
|
|
pass
|
|
else:
|
|
try:
|
|
messagebox.showerror(
|
|
"Launch", f"App not found at {app_path}. Please build it first."
|
|
)
|
|
except Exception:
|
|
pass
|
|
|
|
def run_final_deployment(self):
|
|
initial_dir = str(self._resolve_env_dir("PROLE_DATA", "data"))
|
|
try:
|
|
if not Path(initial_dir).exists():
|
|
initial_dir = str(Path.home())
|
|
except Exception:
|
|
initial_dir = str(Path.home())
|
|
|
|
export_base = filedialog.askdirectory(
|
|
initialdir=initial_dir, title="Select deployment export directory"
|
|
)
|
|
if not export_base:
|
|
self.safe_after(
|
|
lambda: (
|
|
self.bg_canvas.itemconfig(
|
|
self._save_deployment_status_label,
|
|
text="Save canceled.",
|
|
fill="#6e6e73",
|
|
)
|
|
if hasattr(self, "_save_deployment_status_label")
|
|
and self.bg_canvas.winfo_exists()
|
|
else None
|
|
)
|
|
)
|
|
return
|
|
|
|
export_base = str(Path(export_base).expanduser())
|
|
docker_export_dir = str(Path(export_base) / "docker-import")
|
|
helm_export_dir = str(Path(export_base) / "helm-chart")
|
|
kustomize_export_dir = str(Path(export_base) / "kustomize")
|
|
|
|
def worker():
|
|
self.safe_after(
|
|
lambda: (
|
|
self._save_deployment_button.configure(state="disabled")
|
|
if hasattr(self, "_save_deployment_button")
|
|
and self._save_deployment_button.winfo_exists()
|
|
else None
|
|
)
|
|
)
|
|
self.safe_after(
|
|
lambda: (
|
|
self.bg_canvas.itemconfig(
|
|
self._save_deployment_status_label,
|
|
text="Saving deployment...",
|
|
fill="blue",
|
|
)
|
|
if hasattr(self, "_save_deployment_status_label")
|
|
and self.bg_canvas.winfo_exists()
|
|
else None
|
|
)
|
|
)
|
|
|
|
# Use the deploy console on the services screen for visible output
|
|
console = getattr(self, "_cnpg_deploy_console", None)
|
|
|
|
# Fall back to install_consoles tab if available
|
|
script_name = "final_deployment.sh"
|
|
if not console:
|
|
console = getattr(self, "install_consoles", {}).get(script_name)
|
|
if (
|
|
hasattr(self, "install_consoles")
|
|
and script_name in self.install_consoles
|
|
):
|
|
self.safe_after(
|
|
lambda: (
|
|
self.install_tabs.select(
|
|
self.install_consoles[script_name].master
|
|
)
|
|
if hasattr(self, "install_tabs")
|
|
and self.install_tabs.winfo_exists()
|
|
else None
|
|
)
|
|
)
|
|
|
|
env = os.environ.copy()
|
|
env["PROLE_HOME"] = str(PROJECT_ROOT)
|
|
env["DOCKER_IMPORT_DIR"] = docker_export_dir
|
|
env["HELM_CHART_DIR"] = helm_export_dir
|
|
env["KUSTOMIZE_DIR"] = kustomize_export_dir
|
|
|
|
if console:
|
|
console.clear()
|
|
console.write("========================================\n")
|
|
console.write("Save Deployment\n")
|
|
console.write("========================================\n\n")
|
|
console.write(f"Export base: {export_base}\n")
|
|
console.write(f"Docker images: {docker_export_dir}\n")
|
|
console.write(f"Helm chart: {helm_export_dir}\n")
|
|
console.write(f"Kustomize: {kustomize_export_dir}\n\n")
|
|
|
|
def _on_line(line):
|
|
if console:
|
|
console.write(line)
|
|
|
|
# Stage 1: Docker export, Helm chart, Kustomize
|
|
if console:
|
|
console.write("==> Stage 1: Exporting deployment artifacts...\n\n")
|
|
|
|
rc = self.controller.run_script(
|
|
script_name,
|
|
args=[
|
|
"--docker-export",
|
|
"--helm-chart",
|
|
"--kustomize",
|
|
"--output-dir",
|
|
export_base,
|
|
],
|
|
env=env,
|
|
on_line=_on_line,
|
|
)
|
|
|
|
if rc != 0:
|
|
if console:
|
|
console.write(f"\n[ERROR] Artifact export failed with code {rc}\n")
|
|
self.safe_after(
|
|
lambda: (
|
|
self.bg_canvas.itemconfig(
|
|
self._save_deployment_status_label,
|
|
text=f"Failed with code {rc}",
|
|
fill="#ff3b30",
|
|
)
|
|
if hasattr(self, "_save_deployment_status_label")
|
|
and self.bg_canvas.winfo_exists()
|
|
else None
|
|
)
|
|
)
|
|
self.safe_after(
|
|
lambda: (
|
|
self._save_deployment_button.configure(state="normal")
|
|
if hasattr(self, "_save_deployment_button")
|
|
and self._save_deployment_button.winfo_exists()
|
|
else None
|
|
)
|
|
)
|
|
return
|
|
|
|
# Stage 2: Prepare k3s pipeline (OpenTofu + ArgoCD)
|
|
if console:
|
|
console.write(
|
|
"\n==> Stage 2: Preparing k3s pipeline (OpenTofu + ArgoCD)...\n\n"
|
|
)
|
|
|
|
self.safe_after(
|
|
lambda: (
|
|
self.bg_canvas.itemconfig(
|
|
self._save_deployment_status_label,
|
|
text="Preparing k3s pipeline...",
|
|
fill="blue",
|
|
)
|
|
if hasattr(self, "_save_deployment_status_label")
|
|
and self.bg_canvas.winfo_exists()
|
|
else None
|
|
)
|
|
)
|
|
|
|
pipeline_rc = self.controller.run_script(
|
|
script_name, args=["--prepare-k3s-pipeline"], env=env, on_line=_on_line
|
|
)
|
|
|
|
if pipeline_rc != 0:
|
|
if console:
|
|
console.write(
|
|
f"\n[WARN] k3s pipeline preparation failed with code {pipeline_rc}\n"
|
|
)
|
|
console.write(
|
|
"Deployment artifacts were saved, but pipeline staging had issues.\n"
|
|
)
|
|
|
|
if console:
|
|
console.write("\n========================================\n")
|
|
console.write("Save Deployment Complete\n")
|
|
console.write("========================================\n")
|
|
console.write(f"\nArtifacts: {export_base}\n")
|
|
console.write(
|
|
f"Pipeline: {PROJECT_ROOT / 'deploy' / 'opentofu' / 'k3s'}\n"
|
|
)
|
|
console.write(
|
|
f"ArgoCD: {PROJECT_ROOT / 'deploy' / 'opentofu' / 'k3s' / 'argocd'}\n"
|
|
)
|
|
|
|
if rc == 0 and pipeline_rc == 0:
|
|
self.safe_after(
|
|
lambda: (
|
|
self.bg_canvas.itemconfig(
|
|
self._save_deployment_status_label,
|
|
text="Deployment saved successfully.",
|
|
fill="#34c759",
|
|
)
|
|
if hasattr(self, "_save_deployment_status_label")
|
|
and self.bg_canvas.winfo_exists()
|
|
else None
|
|
)
|
|
)
|
|
elif rc == 0:
|
|
self.safe_after(
|
|
lambda: (
|
|
self.bg_canvas.itemconfig(
|
|
self._save_deployment_status_label,
|
|
text="Saved (pipeline warnings)",
|
|
fill="#ff9500",
|
|
)
|
|
if hasattr(self, "_save_deployment_status_label")
|
|
and self.bg_canvas.winfo_exists()
|
|
else None
|
|
)
|
|
)
|
|
else:
|
|
self.safe_after(
|
|
lambda: (
|
|
self.bg_canvas.itemconfig(
|
|
self._save_deployment_status_label,
|
|
text=f"Failed with code {rc}",
|
|
fill="#ff3b30",
|
|
)
|
|
if hasattr(self, "_save_deployment_status_label")
|
|
and self.bg_canvas.winfo_exists()
|
|
else None
|
|
)
|
|
)
|
|
|
|
self.safe_after(
|
|
lambda: (
|
|
self._save_deployment_button.configure(state="normal")
|
|
if hasattr(self, "_save_deployment_button")
|
|
and self._save_deployment_button.winfo_exists()
|
|
else None
|
|
)
|
|
)
|
|
|
|
threading.Thread(target=worker, daemon=True).start()
|
|
|
|
def run_build_a_bao(self):
|
|
def worker():
|
|
self.safe_after(
|
|
lambda: (
|
|
self._build_a_bao_button.configure(state="disabled")
|
|
if hasattr(self, "_build_a_bao_button")
|
|
and self._build_a_bao_button.winfo_exists()
|
|
else None
|
|
)
|
|
)
|
|
self.safe_after(
|
|
lambda: (
|
|
self.bg_canvas.itemconfig(
|
|
self._build_a_bao_status_label,
|
|
text="Saving secrets to OpenBao...",
|
|
fill="blue",
|
|
)
|
|
if hasattr(self, "_build_a_bao_status_label")
|
|
and self.bg_canvas.winfo_exists()
|
|
else None
|
|
)
|
|
)
|
|
|
|
script_name = "build-a-bao.sh"
|
|
if (
|
|
hasattr(self, "install_consoles")
|
|
and script_name in self.install_consoles
|
|
):
|
|
self.safe_after(
|
|
lambda: (
|
|
self.install_tabs.select(
|
|
self.install_consoles[script_name].master
|
|
)
|
|
if hasattr(self, "install_tabs")
|
|
and self.install_tabs.winfo_exists()
|
|
else None
|
|
)
|
|
)
|
|
|
|
env = os.environ.copy()
|
|
env["PROLE_HOME"] = str(PROJECT_ROOT)
|
|
env["PROLE_SERVICE"] = str(PROJECT_ROOT)
|
|
env["NAMESPACE"] = (self.db_namespace.get() or "").strip()
|
|
|
|
console = getattr(self, "install_consoles", {}).get(script_name)
|
|
if console:
|
|
console.clear()
|
|
console.write(f"Running {script_name} ...\n")
|
|
|
|
def _on_line(line):
|
|
if console:
|
|
console.write(line)
|
|
|
|
rc = self.controller.run_script(script_name, env=env, on_line=_on_line)
|
|
|
|
if rc == 0:
|
|
self._secrets_finalized = True
|
|
self.safe_after(
|
|
lambda: (
|
|
self.bg_canvas.itemconfig(
|
|
self._build_a_bao_status_label,
|
|
text="Secrets saved to OpenBao.",
|
|
fill="#34c759",
|
|
)
|
|
if hasattr(self, "_build_a_bao_status_label")
|
|
and self.bg_canvas.winfo_exists()
|
|
else None
|
|
)
|
|
)
|
|
else:
|
|
self.safe_after(
|
|
lambda: (
|
|
self.bg_canvas.itemconfig(
|
|
self._build_a_bao_status_label,
|
|
text=f"Failed with code {rc}",
|
|
fill="#ff3b30",
|
|
)
|
|
if hasattr(self, "_build_a_bao_status_label")
|
|
and self.bg_canvas.winfo_exists()
|
|
else None
|
|
)
|
|
)
|
|
|
|
self.safe_after(
|
|
lambda: (
|
|
self._build_a_bao_button.configure(state="normal")
|
|
if hasattr(self, "_build_a_bao_button")
|
|
and self._build_a_bao_button.winfo_exists()
|
|
else None
|
|
)
|
|
)
|
|
|
|
threading.Thread(target=worker, daemon=True).start()
|
|
|
|
def create_deploy_screen(self):
|
|
"""Create the Deploy screen"""
|
|
frame = tk.Frame(self.content_area, bg="#1a1a1a")
|
|
self.screens["deploy"] = frame
|
|
|
|
# Title
|
|
title = ttk.Label(frame, text="Build and Deploy", style="Title.TLabel")
|
|
title.pack(pady=(0, 30))
|
|
|
|
# Instructions
|
|
instructions = tk.Label(
|
|
frame,
|
|
text="Build and deploy Prole services to k3d cluster",
|
|
bg="#1a1a1a",
|
|
fg="#aaaaaa",
|
|
font=("Helvetica", 11),
|
|
)
|
|
instructions.pack(pady=(0, 20))
|
|
|
|
# Environment selector
|
|
env_frame = tk.Frame(frame, bg="#1a1a1a")
|
|
env_frame.pack(pady=(0, 10), fill="x")
|
|
env_label = tk.Label(
|
|
env_frame,
|
|
text="Target Environment:",
|
|
bg="#1a1a1a",
|
|
fg="#dddddd",
|
|
font=("Helvetica", 11),
|
|
)
|
|
env_label.pack(side="left", padx=(0, 10))
|
|
self.deploy_environment = tk.StringVar(value="Dev")
|
|
env_combo = ttk.Combobox(
|
|
env_frame,
|
|
textvariable=self.deploy_environment,
|
|
values=["Dev", "Service", "Prod"],
|
|
state="readonly",
|
|
width=18,
|
|
)
|
|
env_combo.pack(side="left")
|
|
env_help = tk.Label(
|
|
env_frame,
|
|
text="Dev = local k3d (knoe-dev-cluster), Service = remote k3s at pi.prole.org:6443, Prod = stretch of prole-service-cluster",
|
|
bg="#1a1a1a",
|
|
fg="#888888",
|
|
font=("Helvetica", 9),
|
|
)
|
|
env_help.pack(side="left", padx=(12, 0))
|
|
|
|
# Deploy button
|
|
deploy_btn = tk.Button(
|
|
frame,
|
|
text="Build and Deploy",
|
|
command=self.start_deployment,
|
|
bg="#4a9eff",
|
|
fg="white",
|
|
activebackground="#3a8eef",
|
|
font=("Helvetica", 16, "bold"),
|
|
padx=40,
|
|
pady=20,
|
|
cursor="hand2",
|
|
relief="flat",
|
|
)
|
|
deploy_btn.pack(pady=20)
|
|
|
|
# Progress frame
|
|
progress_frame = tk.Frame(frame, bg="#1a1a1a")
|
|
progress_frame.pack(fill="both", expand=True, pady=20)
|
|
|
|
# Progress list
|
|
# Initialize deploy steps; some names are updated dynamically when deployment starts
|
|
self.deploy_steps = [
|
|
{"name": "Build ProleStatus macOS app", "status": "pending"},
|
|
{"name": "Check Docker is running", "status": "pending"},
|
|
{"name": "Check or configure container registry", "status": "pending"},
|
|
{"name": "Ensure target cluster", "status": "pending"},
|
|
{"name": "Build prole-db Docker image", "status": "pending"},
|
|
{"name": "Tag Docker image for registry", "status": "pending"},
|
|
{"name": "Push image to registry", "status": "pending"},
|
|
{"name": "Import image to k3d cluster (Dev only)", "status": "pending"},
|
|
]
|
|
|
|
self.deploy_widgets = {}
|
|
for step in self.deploy_steps:
|
|
self.create_deploy_step_widget(progress_frame, step)
|
|
|
|
def _create_page_deploy(self):
|
|
f = self._page_container()
|
|
ttk.Label(f, text="Deploy", style="Title.TLabel").pack(
|
|
anchor="w", padx=24, pady=(24, 6)
|
|
)
|
|
ttk.Label(
|
|
f,
|
|
text="Preparing to deploy. We will verify steps and perform actions as needed.",
|
|
style="Body.TLabel",
|
|
wraplength=800,
|
|
).pack(anchor="w", padx=24)
|
|
|
|
# Reuse existing deploy steps UI but on light theme
|
|
self.deploy_steps = [
|
|
{"name": "Build Prole macOS app", "status": "pending"},
|
|
{"name": "Check Docker is running", "status": "pending"},
|
|
{"name": "Check or configure container registry", "status": "pending"},
|
|
{"name": "Ensure target cluster", "status": "pending"},
|
|
{"name": "Build prole-db Docker image", "status": "pending"},
|
|
{"name": "Tag Docker image for registry", "status": "pending"},
|
|
{"name": "Push image to registry", "status": "pending"},
|
|
{"name": "Import image to k3d cluster (Dev only)", "status": "pending"},
|
|
{
|
|
"name": "Install LaunchAgent for port-forwards (Dev)",
|
|
"status": "pending",
|
|
},
|
|
]
|
|
self.deploy_widgets = {}
|
|
container = ttk.Frame(f)
|
|
container.pack(fill="both", expand=True, padx=16, pady=8)
|
|
for step in self.deploy_steps:
|
|
self._create_deploy_row(container, step)
|
|
|
|
# self._register_page('deploy', f)
|
|
|
|
def create_deploy_step_widget(self, parent, step):
|
|
"""Create a widget for a deployment step"""
|
|
step_frame = tk.Frame(parent, bg="#2a2a2a", relief="flat")
|
|
step_frame.pack(fill="x", pady=5, padx=10)
|
|
|
|
# Status indicator
|
|
status_canvas = tk.Canvas(
|
|
step_frame, width=30, height=30, bg="#2a2a2a", highlightthickness=0
|
|
)
|
|
status_canvas.pack(side="left", padx=15, pady=15)
|
|
|
|
# Step name
|
|
name_label = tk.Label(
|
|
step_frame,
|
|
text=step["name"],
|
|
bg="#2a2a2a",
|
|
fg="#ffffff",
|
|
font=("Helvetica", 11),
|
|
anchor="w",
|
|
)
|
|
name_label.pack(side="left", fill="x", expand=True, padx=10)
|
|
|
|
# Status text
|
|
status_label = tk.Label(
|
|
step_frame,
|
|
text="Pending",
|
|
bg="#2a2a2a",
|
|
fg="#aaaaaa",
|
|
font=("Helvetica", 10),
|
|
)
|
|
status_label.pack(side="right", padx=15)
|
|
|
|
self.deploy_widgets[step["name"]] = {
|
|
"canvas": status_canvas,
|
|
"label": status_label,
|
|
"step": step,
|
|
}
|
|
|
|
# Draw initial pending state
|
|
self.update_deploy_step_status(step["name"], "pending")
|
|
|
|
def _create_deploy_row(self, parent, step):
|
|
row = ttk.Frame(parent)
|
|
row.pack(fill="x", pady=6)
|
|
canvas = tk.Canvas(row, width=20, height=20, highlightthickness=0)
|
|
canvas.pack(side="left", padx=8)
|
|
lbl = ttk.Label(row, text=step["name"], style="Body.TLabel")
|
|
lbl.pack(side="left")
|
|
status = ttk.Label(row, text="Pending", style="Dim.TLabel")
|
|
status.pack(side="right", padx=8)
|
|
self.deploy_widgets[step["name"]] = {
|
|
"canvas": canvas,
|
|
"label": status,
|
|
"step": step,
|
|
}
|
|
self._draw_status(canvas, "pending")
|
|
|
|
def update_deploy_step_status(self, step_name, status):
|
|
"""Update the status of a deployment step"""
|
|
widget = self.deploy_widgets[step_name]
|
|
canvas = widget["canvas"]
|
|
label = widget["label"]
|
|
step = widget["step"]
|
|
|
|
step["status"] = status
|
|
canvas.delete("all")
|
|
|
|
if status == "pending":
|
|
ui.canvas_oval_on(canvas, 5, 5, 25, 25, outline="#666", width=2)
|
|
label.configure(text="Pending", fg="#aaaaaa")
|
|
elif status == "running":
|
|
ui.canvas_oval_on(
|
|
canvas, 5, 5, 25, 25, outline="#ffaa00", width=2, fill="#ffaa00"
|
|
)
|
|
label.configure(text="Running...", fg="#ffaa00")
|
|
elif status == "completed":
|
|
ui.canvas_oval_on(
|
|
canvas, 5, 5, 25, 25, outline="#28a745", width=2, fill="#28a745"
|
|
)
|
|
ui.canvas_text_on(
|
|
canvas, 15, 15, "✓", fill="white", font=("Helvetica", 16, "bold")
|
|
)
|
|
label.configure(text="Completed", fg="#28a745")
|
|
elif status == "error":
|
|
ui.canvas_oval_on(
|
|
canvas, 5, 5, 25, 25, outline="#dc3545", width=2, fill="#dc3545"
|
|
)
|
|
ui.canvas_text_on(
|
|
canvas, 15, 15, "✗", fill="white", font=("Helvetica", 16, "bold")
|
|
)
|
|
label.configure(text="Error", fg="#dc3545")
|
|
elif status == "skipped":
|
|
ui.canvas_oval_on(
|
|
canvas, 5, 5, 25, 25, outline="#666", width=2, fill="#444444"
|
|
)
|
|
label.configure(text="Skipped", fg="#888888")
|
|
|
|
def start_deployment(self):
|
|
"""Start the deployment process"""
|
|
threading.Thread(target=self.run_deployment, daemon=True).start()
|
|
|
|
def generate_prole_properties(self, env: str):
|
|
"""Dynamically generate prole-tools-app/prole.properties based on environment"""
|
|
props_path = Path("prole-tools-app/prole.properties")
|
|
|
|
# Determine host based on environment
|
|
# For now use localhost as a placeholder for Service/Prod
|
|
host = "localhost"
|
|
|
|
content = f"""# Prole default endpoints (dynamically generated by install.py)
|
|
# UI assets
|
|
icon=img/proleIcon.png
|
|
background=img/proleLogoSepia.png
|
|
|
|
# Dev port-forward supervision
|
|
pf.enabled=true
|
|
|
|
# Service endpoints (7 traffic lights)
|
|
svc.1.name=K3D
|
|
svc.1.host={host}
|
|
svc.1.port=6443
|
|
|
|
svc.2.name=Prometheus
|
|
svc.2.host={host}
|
|
svc.2.port=9090
|
|
|
|
svc.3.name=Grafana
|
|
svc.3.host={host}
|
|
svc.3.port=3000
|
|
|
|
svc.4.name=OpenBAO
|
|
svc.4.host={host}
|
|
svc.4.port=8200
|
|
|
|
svc.5.name=PostgreSQL
|
|
svc.5.host={host}
|
|
svc.5.port=5432
|
|
|
|
svc.6.name=Kong
|
|
svc.6.host={host}
|
|
svc.6.port=8000
|
|
|
|
svc.7.name=CertManager
|
|
svc.7.host={host}
|
|
svc.7.port=9402
|
|
|
|
# Kerberos configuration
|
|
kerberos.enabled={str(self.kerberos_enabled.get()).lower()}
|
|
kerberos.realm={self.kerberos_realm.get()}
|
|
kerberos.user={self.kerberos_user.get()}
|
|
kerberos.kdc={self.kerberos_kdc.get()}
|
|
"""
|
|
props_path.write_text(content)
|
|
print(f"Generated {props_path} for {env} environment")
|
|
|
|
def run_deployment(self):
|
|
"""Run the deployment steps"""
|
|
try:
|
|
# Capture environment selection and prepare dynamic labels
|
|
env = self.deploy_environment.get().strip()
|
|
if env not in ("Dev", "Service", "Prod"):
|
|
env = "Dev"
|
|
|
|
# Update step labels to reflect environment
|
|
self.deploy_widgets["Ensure target cluster"]["step"][
|
|
"name"
|
|
] = f"Ensure target cluster ({env})"
|
|
# Use the correct way to update the label text in the UI
|
|
self.deploy_widgets["Ensure target cluster"][
|
|
"label"
|
|
].master.winfo_children()[1].configure(
|
|
text=f"Ensure target cluster ({env})"
|
|
)
|
|
|
|
# Step 0: Generate prole.properties
|
|
self.generate_prole_properties(env)
|
|
|
|
# Step 1: Build Prole macOS app
|
|
self.update_deploy_step_status("Build Prole macOS app", "running")
|
|
self.build_prole_app()
|
|
self.update_deploy_step_status("Build Prole macOS app", "completed")
|
|
|
|
# Step 1: Check Docker
|
|
self.update_deploy_step_status("Check Docker is running", "running")
|
|
if not self.check_docker_running():
|
|
self.update_deploy_step_status("Check Docker is running", "error")
|
|
messagebox.showerror(
|
|
"Error", "Docker is not running. Please start Docker Desktop."
|
|
)
|
|
return
|
|
self.update_deploy_step_status("Check Docker is running", "completed")
|
|
|
|
# Step 2: Registry discovery/config
|
|
self.update_deploy_step_status(
|
|
"Check or configure container registry", "running"
|
|
)
|
|
self.registry_url = self.ensure_registry_available(env)
|
|
self.update_deploy_step_status(
|
|
"Check or configure container registry", "completed"
|
|
)
|
|
|
|
# Step 3: Ensure target cluster as per environment
|
|
self.update_deploy_step_status("Ensure target cluster", "running")
|
|
self.create_or_select_cluster(env)
|
|
self.update_deploy_step_status("Ensure target cluster", "completed")
|
|
|
|
# Step 3: Build Docker image
|
|
self.update_deploy_step_status("Build prole-db Docker image", "running")
|
|
self.build_docker_image()
|
|
self.update_deploy_step_status("Build prole-db Docker image", "completed")
|
|
|
|
# Step 4: Tag image
|
|
self.update_deploy_step_status("Tag Docker image for registry", "running")
|
|
self.tag_docker_image()
|
|
self.update_deploy_step_status("Tag Docker image for registry", "completed")
|
|
|
|
# Step 5: Push to registry
|
|
self.update_deploy_step_status("Push image to registry", "running")
|
|
self.push_docker_image()
|
|
self.update_deploy_step_status("Push image to registry", "completed")
|
|
|
|
# Step 6: Import to k3d (Dev only)
|
|
if env == "Dev":
|
|
self.update_deploy_step_status(
|
|
"Import image to k3d cluster (Dev only)", "running"
|
|
)
|
|
self.import_k3d_image(cluster_name="knoe-dev-cluster")
|
|
self.update_deploy_step_status(
|
|
"Import image to k3d cluster (Dev only)", "completed"
|
|
)
|
|
else:
|
|
self.update_deploy_step_status(
|
|
"Import image to k3d cluster (Dev only)", "skipped"
|
|
)
|
|
|
|
# Step 7: Install LaunchAgent for port-forwards (Dev)
|
|
if env == "Dev":
|
|
self.update_deploy_step_status(
|
|
"Install LaunchAgent for port-forwards (Dev)", "running"
|
|
)
|
|
ok = self.install_launchagent_port_forwards()
|
|
if ok:
|
|
self.update_deploy_step_status(
|
|
"Install LaunchAgent for port-forwards (Dev)", "completed"
|
|
)
|
|
else:
|
|
self.update_deploy_step_status(
|
|
"Install LaunchAgent for port-forwards (Dev)", "error"
|
|
)
|
|
return
|
|
else:
|
|
self.update_deploy_step_status(
|
|
"Install LaunchAgent for port-forwards (Dev)", "skipped"
|
|
)
|
|
|
|
messagebox.showinfo("Success", "Deployment completed successfully!")
|
|
|
|
except Exception as e:
|
|
messagebox.showerror("Error", f"Deployment failed: {str(e)}")
|
|
|
|
def install_launchagent_port_forwards(self) -> bool:
|
|
"""Create/update the user LaunchAgent and helper script to manage kubectl port-forwards.
|
|
- Helper script: ~/Library/Application Support/Prole/bin/prole-kpf.sh
|
|
- LaunchAgent: ~/Library/LaunchAgents/org.prole.prole-db.kpf-dev.plist
|
|
"""
|
|
try:
|
|
home = Path.home()
|
|
bin_dir = home / "Library" / "Application Support" / "Prole" / "bin"
|
|
run_dir = home / "Library" / "Application Support" / "Prole" / "run"
|
|
plist_path = (
|
|
home / "Library" / "LaunchAgents" / "org.prole.prole-db.kpf-dev.plist"
|
|
)
|
|
script_path = bin_dir / "prole-kpf.sh"
|
|
|
|
bin_dir.mkdir(parents=True, exist_ok=True)
|
|
run_dir.mkdir(parents=True, exist_ok=True)
|
|
plist_path.parent.mkdir(parents=True, exist_ok=True)
|
|
|
|
helper_script = """#!/bin/sh
|
|
set -eu
|
|
|
|
LABEL="org.prole.prole-db.kpf-dev"
|
|
PLIST="$HOME/Library/LaunchAgents/$LABEL.plist"
|
|
RUNDIR="$HOME/Library/Application Support/Prole/run"
|
|
PIDFILE="$RUNDIR/kpf.pids"
|
|
|
|
ensure_rundir() {
|
|
mkdir -p "$RUNDIR"
|
|
}
|
|
|
|
list_cmds() {
|
|
# Enumerate ProleCommands array using PlistBuddy
|
|
if /usr/libexec/PlistBuddy -c "Print :ProleCommands" "$PLIST" >/dev/null 2>&1; then
|
|
i=0
|
|
while true; do
|
|
if ! val=$(/usr/libexec/PlistBuddy -c "Print :ProleCommands:$i" "$PLIST" 2>/dev/null); then
|
|
break
|
|
fi
|
|
echo "$val"
|
|
i=$((i+1))
|
|
done
|
|
fi
|
|
}
|
|
|
|
start() {
|
|
ensure_rundir
|
|
: > "$PIDFILE"
|
|
IFS='\n'
|
|
for cmd in $(list_cmds); do
|
|
[ -z "$cmd" ] && continue
|
|
(sh -lc "$cmd") &
|
|
echo $! >> "$PIDFILE"
|
|
done
|
|
wait
|
|
}
|
|
|
|
stop() {
|
|
if [ -f "$PIDFILE" ]; then
|
|
while read -r pid; do
|
|
[ -z "$pid" ] && continue
|
|
kill "$pid" 2>/dev/null || true
|
|
done < "$PIDFILE"
|
|
rm -f "$PIDFILE"
|
|
fi
|
|
}
|
|
|
|
status() {
|
|
if [ ! -f "$PIDFILE" ]; then
|
|
echo "not running"
|
|
exit 3
|
|
fi
|
|
alive=0
|
|
total=0
|
|
while read -r pid; do
|
|
[ -z "$pid" ] && continue
|
|
total=$((total+1))
|
|
if kill -0 "$pid" 2>/dev/null; then alive=$((alive+1)); fi
|
|
done < "$PIDFILE"
|
|
echo "$alive/$total running"
|
|
}
|
|
|
|
case "${1:-}" in
|
|
start) start ;;
|
|
stop) stop ;;
|
|
restart) stop; start ;;
|
|
status) status ;;
|
|
*) echo "Usage: $0 {start|stop|restart|status}" >&2; exit 2 ;;
|
|
esac
|
|
"""
|
|
|
|
script_path.write_text(helper_script)
|
|
os.chmod(script_path, 0o755)
|
|
|
|
# Default commands for Dev environment (k3d)
|
|
default_cmds = [
|
|
"kubectl port-forward -n monitoring svc/kps-kube-prometheus-stack-prometheus 9090",
|
|
"kubectl -n kubernetes-dashboard port-forward svc/kubernetes-dashboard-kong-proxy 8443:443",
|
|
"kubectl port-forward svc/prole-db-rw 5432:5432 --address 0.0.0.0",
|
|
"kubectl port-forward -n monitoring svc/kps-grafana 3000:80 --address 0.0.0.0",
|
|
]
|
|
|
|
# Create plist dict
|
|
from plistlib import dumps as plist_dumps
|
|
|
|
plist_dict = {
|
|
"Label": "org.prole.prole-db.kpf-dev",
|
|
"RunAtLoad": True,
|
|
"KeepAlive": True,
|
|
# Ensure PATH has common locations for kubectl
|
|
"EnvironmentVariables": {
|
|
"PATH": os.environ.get("PATH", "/usr/local/bin:/usr/bin:/bin")
|
|
},
|
|
"StandardOutPath": str(
|
|
home / "Library" / "Logs" / "org.prole.prole-db.kpf-dev.out.log"
|
|
),
|
|
"StandardErrorPath": str(
|
|
home / "Library" / "Logs" / "org.prole.prole-db.kpf-dev.err.log"
|
|
),
|
|
"ProgramArguments": [str(script_path), "start"],
|
|
"ProleCommands": default_cmds,
|
|
}
|
|
|
|
plist_bytes = plist_dumps(plist_dict)
|
|
plist_path.write_bytes(plist_bytes)
|
|
|
|
# Reload the agent
|
|
uid = os.getuid()
|
|
# Try to kickstart first
|
|
subprocess.run(
|
|
[
|
|
"launchctl",
|
|
"kickstart",
|
|
"-k",
|
|
f"gui/{uid}/org.prole.prole-db.kpf-dev",
|
|
],
|
|
capture_output=True,
|
|
)
|
|
# Bootout and bootstrap to ensure it's loaded, then kickstart
|
|
subprocess.run(
|
|
[
|
|
"launchctl",
|
|
"bootout",
|
|
f"gui/{uid}",
|
|
f"gui/{uid}/org.prole.prole-db.kpf-dev",
|
|
],
|
|
capture_output=True,
|
|
)
|
|
subprocess.run(
|
|
["launchctl", "bootstrap", f"gui/{uid}", str(plist_path)], check=True
|
|
)
|
|
subprocess.run(
|
|
[
|
|
"launchctl",
|
|
"kickstart",
|
|
"-k",
|
|
f"gui/{uid}/org.prole.prole-db.kpf-dev",
|
|
],
|
|
check=True,
|
|
)
|
|
|
|
return True
|
|
except Exception as e:
|
|
print(f"LaunchAgent setup failed: {e}")
|
|
return False
|
|
|
|
def _draw_status(self, canvas, status):
|
|
ui.canvas_clear(canvas)
|
|
if status == "success":
|
|
ui.canvas_oval_on(canvas, 2, 2, 18, 18, fill="#34c759", outline="")
|
|
ui.canvas_line_on(canvas, 5, 10, 9, 14, fill="white", width=2)
|
|
ui.canvas_line_on(canvas, 9, 14, 16, 6, fill="white", width=2)
|
|
elif status == "running":
|
|
ui.canvas_oval_on(canvas, 2, 2, 18, 18, fill="#ffd60a", outline="")
|
|
elif status == "error":
|
|
ui.canvas_oval_on(canvas, 2, 2, 18, 18, fill="#ff3b30", outline="")
|
|
else:
|
|
ui.canvas_oval_on(canvas, 2, 2, 18, 18, outline="#b0b0b0")
|
|
|
|
# --------------- Dependency helpers ---------------
|