"""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)