mirror of
https://github.com/dredx/prole.git
synced 2026-09-24 19:54:32 +00:00
435 lines
16 KiB
Python
435 lines
16 KiB
Python
"""GitOps (Gitea / GitLab) configuration and deployment screen."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
import subprocess
|
|
import threading
|
|
import tkinter as tk
|
|
from tkinter import ttk
|
|
|
|
from knoe import prole_conf
|
|
from knoe import screen as ui
|
|
from knoe.core.env import PROJECT_ROOT, _normalize_cluster_env, _parse_bool
|
|
|
|
|
|
class GitOpsScreenMixin:
|
|
"""GitOps deployment screen."""
|
|
|
|
# ------------------------------------------------------------------
|
|
# GitOps provider choice screen
|
|
# ------------------------------------------------------------------
|
|
def _render_gitops_choice_page(self):
|
|
"""Explicit GitOps provider selection: Gitea, ArgoCD, or None."""
|
|
content_width = self.bg_canvas.winfo_width() or 975
|
|
right_margin = content_width - 48
|
|
ui.canvas_text(
|
|
self,
|
|
right_margin,
|
|
40,
|
|
"knoe.dev",
|
|
fill="#6e6e73",
|
|
font=("SF Pro Text", 32, "bold"),
|
|
anchor="ne",
|
|
)
|
|
ui.canvas_text(
|
|
self,
|
|
right_margin,
|
|
85,
|
|
"infrastructure.auto()",
|
|
fill="#6e6e73",
|
|
font=("SF Pro Text", 18),
|
|
anchor="ne",
|
|
)
|
|
self._render_title("GitOps Provider", y=150)
|
|
description = (
|
|
"Choose your GitOps provider for this deployment.\n"
|
|
"Gitea deploys a self-hosted Git server; ArgoCD adds continuous delivery.\n"
|
|
"Select None to skip GitOps tooling entirely."
|
|
)
|
|
self._render_paragraph(description, y=210)
|
|
|
|
x_label = 48
|
|
y = 310
|
|
|
|
for label, value in [
|
|
("Gitea — self-hosted Git + CI (lightweight)", "gitea"),
|
|
("GitLab — full DevOps platform via GitLab Operator", "gitlab"),
|
|
("ArgoCD — continuous delivery / GitOps sync", "argocd"),
|
|
("None — skip GitOps tooling", "none"),
|
|
]:
|
|
rb = tk.Radiobutton(
|
|
self.bg_canvas,
|
|
text=label,
|
|
variable=self.gitops_provider,
|
|
value=value,
|
|
command=self._on_gitops_provider_change,
|
|
bg="white",
|
|
fg="black",
|
|
activebackground="white",
|
|
selectcolor="white",
|
|
font=("SF Pro Text", 12),
|
|
)
|
|
win = self.bg_canvas.create_window(x_label, y, window=rb, anchor="nw")
|
|
self._canvas_items.append(win)
|
|
self._overlay_widgets.append(rb)
|
|
y += 36
|
|
|
|
def _on_gitops_provider_change(self):
|
|
"""Persist the chosen GitOps provider to prole_cfg."""
|
|
provider = self.gitops_provider.get()
|
|
try:
|
|
if "Optional Features" not in self.prole_cfg_data:
|
|
self.prole_cfg_data["Optional Features"] = {}
|
|
self.prole_cfg_data["Optional Features"]["GITOPS_PROVIDER"] = provider
|
|
# Keep legacy flags in sync so downstream steps remain consistent.
|
|
self.prole_cfg_data["Optional Features"]["GITOPS_ENABLED"] = str(
|
|
provider in ("gitea", "gitlab")
|
|
)
|
|
self.prole_cfg_data["Optional Features"]["ARGOCD_ENABLED"] = str(
|
|
provider == "argocd"
|
|
)
|
|
self._save_prole_cfg()
|
|
except Exception:
|
|
pass
|
|
# Sync boolean vars used by the individual feature screens.
|
|
self.gitops_enabled.set(provider in ("gitea", "gitlab"))
|
|
self.argocd_enabled.set(provider == "argocd")
|
|
|
|
# ------------------------------------------------------------------
|
|
# Gitea deployment screen
|
|
# ------------------------------------------------------------------
|
|
def _render_gitops_config_page(self):
|
|
content_width = self.bg_canvas.winfo_width() or 975
|
|
right_margin = content_width - 48
|
|
|
|
ui.canvas_text(
|
|
self,
|
|
right_margin,
|
|
40,
|
|
"knoe.dev",
|
|
fill="#6e6e73",
|
|
font=("SF Pro Text", 32, "bold"),
|
|
anchor="ne",
|
|
)
|
|
ui.canvas_text(
|
|
self,
|
|
right_margin,
|
|
85,
|
|
"infrastructure.auto()",
|
|
fill="#6e6e73",
|
|
font=("SF Pro Text", 18),
|
|
anchor="ne",
|
|
)
|
|
|
|
self._render_title("GitOps", y=150)
|
|
|
|
provider = getattr(self, "gitops_provider", None)
|
|
provider_name = (provider.get() if provider else None) or "gitea"
|
|
if provider_name == "gitlab":
|
|
deploy_description = (
|
|
"Deploys self-hosted GitLab via the GitLab Operator (etc/init_gitlab.sh).\n"
|
|
"Removes any pre-existing Gitea/git.prole.org config before installing.\n"
|
|
"Uses knoe-db (CloudNativePG) as the PostgreSQL data store."
|
|
)
|
|
else:
|
|
deploy_description = (
|
|
"Deploys self-hosted GitOps via etc/init_gitea.sh.\n"
|
|
"Select your GitOps provider and node placement below."
|
|
)
|
|
description = deploy_description
|
|
self._render_paragraph(description, y=210)
|
|
|
|
enable_cb = tk.Checkbutton(
|
|
self.bg_canvas,
|
|
text="Enable GitOps",
|
|
variable=self.gitops_enabled,
|
|
command=self._on_gitops_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)
|
|
|
|
# GitOps provider dropdown
|
|
ui.canvas_text(
|
|
self,
|
|
48,
|
|
290,
|
|
"GitOps Provider",
|
|
fill="black",
|
|
font=("SF Pro Text", 12, "bold"),
|
|
)
|
|
provider_combo = ttk.Combobox(
|
|
self.bg_canvas,
|
|
textvariable=self.gitops_git_provider,
|
|
values=["Gitea", "GitLab", "Forgejo"],
|
|
state="readonly",
|
|
font=("SF Pro Text", 11),
|
|
width=20,
|
|
)
|
|
provider_window = self.bg_canvas.create_window(
|
|
48, 315, window=provider_combo, anchor="nw", width=220, height=30
|
|
)
|
|
self._canvas_items.append(provider_window)
|
|
self._overlay_widgets.append(provider_combo)
|
|
|
|
node_selector_var = getattr(self, "gitops_node_selector", None)
|
|
if hasattr(self, "_render_k3s_node_selector") and node_selector_var is not None:
|
|
self._render_k3s_node_selector(
|
|
y=360,
|
|
target_var=node_selector_var,
|
|
title="Node selector:",
|
|
)
|
|
|
|
initial_status = "Ready" if self.gitops_enabled.get() else "Disabled"
|
|
self._gitops_status_var = tk.StringVar(value=initial_status)
|
|
status_item = ui.canvas_text(
|
|
self,
|
|
48,
|
|
415,
|
|
f"Status: {initial_status}",
|
|
fill="black",
|
|
font=("SF Pro Text", 12),
|
|
)
|
|
|
|
def update_status_text(*_):
|
|
try:
|
|
self.bg_canvas.itemconfig(
|
|
status_item, text=f"Status: {self._gitops_status_var.get()}"
|
|
)
|
|
except Exception:
|
|
pass
|
|
|
|
self._gitops_status_var.trace_add("write", update_status_text)
|
|
|
|
# Console output
|
|
self._gitops_console = self._create_console_output(
|
|
y=455, title="Deployment Output", width=880, height=280
|
|
)
|
|
|
|
# Deploy button
|
|
self._gitops_deploy_button = tk.Button(
|
|
self.bg_canvas,
|
|
text="Deploy GitOps",
|
|
command=self.run_gitops_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, 760, window=self._gitops_deploy_button, anchor="nw"
|
|
)
|
|
self._overlay_widgets.append(self._gitops_deploy_button)
|
|
self._canvas_items.append(btn_window)
|
|
|
|
def run_gitops_deploy(self):
|
|
if getattr(self, "_gitops_deploying", False):
|
|
return
|
|
if not self.gitops_enabled.get():
|
|
self._gitops_status_var.set("Disabled")
|
|
self.update_footer()
|
|
return
|
|
|
|
self._gitops_deploying = True
|
|
self._gitops_deploy_button.configure(state="disabled")
|
|
self._gitops_status_var.set("Deploying...")
|
|
self._gitops_success = False
|
|
self._gitops_console.clear()
|
|
self.update_footer()
|
|
|
|
def worker():
|
|
# gitops_git_provider is the combobox on the config page (Gitea/GitLab/Forgejo).
|
|
# gitops_provider is the radio-button choice page (gitea/gitlab/argocd/none).
|
|
# Prefer gitops_git_provider since that's what the user sees and sets.
|
|
git_provider = (
|
|
getattr(self, "gitops_git_provider", None) and
|
|
self.gitops_git_provider.get()
|
|
) or ""
|
|
if not git_provider:
|
|
git_provider = (
|
|
getattr(self, "gitops_provider", None) and
|
|
self.gitops_provider.get()
|
|
) or "Gitea"
|
|
is_gitlab = git_provider.lower() == "gitlab"
|
|
|
|
if is_gitlab:
|
|
namespace = os.environ.get("GITLAB_NAMESPACE") or "gitlab"
|
|
else:
|
|
namespace = os.environ.get("GITEA_NAMESPACE") or "gitea"
|
|
|
|
env = os.environ.copy()
|
|
env["PROLE_HOME"] = str(PROJECT_ROOT)
|
|
env["PROLE_SERVICE"] = str(PROJECT_ROOT)
|
|
env["NAMESPACE"] = namespace
|
|
if is_gitlab:
|
|
env["GITLAB_NAMESPACE"] = namespace
|
|
else:
|
|
env["GITEA_NAMESPACE"] = namespace
|
|
|
|
if is_gitlab:
|
|
self._gitops_console.write("Starting GitLab deployment via GitLab Operator...\n")
|
|
script_path = PROJECT_ROOT / "etc" / "init_gitlab.sh"
|
|
else:
|
|
self._gitops_console.write("Starting Gitea deployment...\n")
|
|
script_path = PROJECT_ROOT / "etc" / "init_gitea.sh"
|
|
|
|
if not script_path.exists():
|
|
self._gitops_console.write(f"Error: {script_path} not found.\n")
|
|
self.safe_after(
|
|
lambda: self._gitops_status_var.set("Failed (Script not found)")
|
|
)
|
|
self.safe_after(
|
|
lambda: self._gitops_deploy_button.configure(state="normal")
|
|
)
|
|
self._gitops_deploying = False
|
|
return
|
|
|
|
mode = (os.environ.get("PROLE_MODE") or "").strip()
|
|
if not mode:
|
|
cluster_env = _normalize_cluster_env(self.cluster_env.get())
|
|
if cluster_env == "dev":
|
|
mode = "k3d"
|
|
elif cluster_env in ("service", "prod"):
|
|
mode = "k3s" if cluster_env == "service" else "k8s"
|
|
else:
|
|
mode = "k3d"
|
|
args = ["--mode", mode, "--namespace", namespace]
|
|
node_selector = (self.gitops_node_selector.get() or "").strip()
|
|
if node_selector:
|
|
if is_gitlab:
|
|
env["GITLAB_NODE_SELECTOR"] = node_selector
|
|
env["NODE_SELECTOR"] = node_selector
|
|
args.extend(["--node-selector", node_selector])
|
|
elif mode == "k3s":
|
|
env["GITEA_NODE_SELECTOR"] = node_selector
|
|
env["NODE_SELECTOR"] = node_selector
|
|
self._gitops_console.write(f"Node selector: {node_selector}\n")
|
|
|
|
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)])
|
|
|
|
if _parse_bool(os.environ.get("GITEA_FOREGROUND"), False):
|
|
args.append("--foreground")
|
|
|
|
self._gitops_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._gitops_console.write(f"Failed to start deploy: {e}\n")
|
|
self.safe_after(
|
|
lambda: self._gitops_status_var.set("Failed (Launch error)")
|
|
)
|
|
self.safe_after(
|
|
lambda: self._gitops_deploy_button.configure(state="normal")
|
|
)
|
|
self._gitops_deploying = False
|
|
return
|
|
|
|
if proc.stdout:
|
|
for line in iter(proc.stdout.readline, ""):
|
|
if line:
|
|
self._gitops_console.write(line)
|
|
proc.stdout.close()
|
|
rc = proc.wait()
|
|
|
|
if rc == 0:
|
|
self._gitops_success = True
|
|
self.safe_after(
|
|
lambda: self._gitops_status_var.set("Deployed Successfully")
|
|
)
|
|
label = "GitLab" if is_gitlab else "GitOps"
|
|
self._gitops_console.write(
|
|
f"\n{label} deployment completed successfully.\n"
|
|
)
|
|
else:
|
|
self._gitops_success = False
|
|
self.safe_after(
|
|
lambda: self._gitops_status_var.set(f"Failed (Code {rc})")
|
|
)
|
|
label = "GitLab" if is_gitlab else "GitOps"
|
|
self._gitops_console.write(
|
|
f"\n{label} deployment failed with exit code {rc}.\n"
|
|
)
|
|
|
|
self._gitops_deploying = False
|
|
self.safe_after(
|
|
lambda: self._gitops_deploy_button.configure(state="normal")
|
|
)
|
|
self.safe_after(self.update_footer)
|
|
|
|
threading.Thread(target=worker, daemon=True).start()
|
|
|
|
def _on_gitops_toggle(self):
|
|
enabled = self.gitops_enabled.get()
|
|
try:
|
|
self.prole_cfg_data["Optional Features"]["GITOPS_ENABLED"] = str(enabled)
|
|
self._save_prole_cfg()
|
|
except Exception:
|
|
pass
|
|
|
|
if not enabled:
|
|
self._gitops_success = False
|
|
if hasattr(self, "_gitops_status_var"):
|
|
self._gitops_status_var.set("Disabled")
|
|
try:
|
|
self._gitops_console.write("GitOps disabled. Deployment skipped.\n")
|
|
except Exception:
|
|
pass
|
|
self.update_footer()
|
|
return
|
|
|
|
if hasattr(self, "_gitops_status_var"):
|
|
self._gitops_status_var.set("Enabled")
|
|
try:
|
|
self._gitops_console.clear()
|
|
self._gitops_console.write("GitOps enabled. Starting deployment...\n")
|
|
except Exception:
|
|
pass
|
|
self.update_footer()
|
|
self.safe_after(self.run_gitops_deploy)
|