"""Milestone abstraction for the UI-agnostic installer core.""" from __future__ import annotations from abc import ABC, abstractmethod from typing import Callable, Sequence from . import config as inst_config from .state import InstallerState ProgressCallback = Callable[[str, float | None], None] ValidationResult = str | Sequence[str] | None class Milestone(ABC): """Base class for installer milestones. Milestones encapsulate business logic, validation, and flow control. They must not import or depend on UI toolkits. """ id: str title: str def __init__(self, milestone_id: str, title: str): self.id = milestone_id self.title = title def validate(self, state: InstallerState) -> ValidationResult: """Return a validation error or None when valid.""" return None @abstractmethod def execute(self, state: InstallerState, progress: ProgressCallback | None = None) -> None: """Execute milestone logic. Use the progress callback to emit updates.""" raise NotImplementedError def next(self, state: InstallerState) -> str | None: """Return the next milestone id or None to finish.""" return None def _parse_bool(self, val: any, default: bool | None = False) -> bool | None: return inst_config._parse_bool(val, default) def _run_cmd(self, cmd: str | list[str], cwd: str | None = None, on_stdout: callable | None = None) -> int: if isinstance(cmd, str): cmd = ['bash', '-c', cmd] import subprocess try: proc = subprocess.Popen( cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, bufsize=1, cwd=cwd ) for line in iter(proc.stdout.readline, ''): if on_stdout: on_stdout(line) return proc.wait() except Exception: return 1 def _get_script_env(self, state: InstallerState) -> dict: import os from pathlib import Path env = os.environ.copy() project_root = inst_config.PROJECT_ROOT env["PROLE_HOME"] = str(project_root) env["PROLE_SERVICE"] = str(project_root) namespace = state.inputs.get('init_password.db_namespace', 'default') env["NAMESPACE"] = namespace # Paths env["PROLE_CONF"] = state.inputs.get('env_setup.PROLE_CONF', str(project_root / "conf")) env["PROLE_DATA"] = state.inputs.get('env_setup.PROLE_DATA', str(project_root / "prole-db" / "data")) env["PROLE_LOGS"] = state.inputs.get('env_setup.PROLE_LOGS', str(project_root / "logs")) env["PROLE_SERVICE"] = state.inputs.get('env_setup.PROLE_SERVICE', str(project_root / "etc")) service_ns = (state.config_data.get('Global', {}) or {}).get('SERVICE_NAMESPACE', '').strip() if not service_ns: service_ns = (os.environ.get('SERVICE_NAMESPACE') or '').strip() if not service_ns: service_ns = namespace or 'default' env["SERVICE_NAMESPACE"] = service_ns from installer.core.env import _deployment_mode_from_env cluster_env = state.inputs.get('init_cluster.cluster_env', 'dev') mode = _deployment_mode_from_env(cluster_env) or 'k3d' env["PROLE_MODE"] = mode # Kubeconfig if mode == "k3s": kc = Path(env["PROLE_SERVICE"]) / "secrets" / "k3s.kubeconfig" if not kc.exists(): kc = project_root / "etc" / "secrets" / "k3s.kubeconfig" if kc.exists(): env["KUBECONFIG"] = str(kc) elif mode == "k3d": # Ensure k3d dev clusters have a resolvable KUBECONFIG. # The cluster may already be running from a previous session; # merge the kubeconfig so downstream scripts can reach it. if not (env.get("KUBECONFIG") or "").strip(): import subprocess as _sp cluster_name = "prole-dev-cluster" try: _sp.run( ['k3d', 'kubeconfig', 'merge', cluster_name, '--kubeconfig-switch-context'], capture_output=True, text=True, timeout=10, ) except Exception: pass default_kube = str(Path.home() / '.kube' / 'config') if Path(default_kube).exists(): env["KUBECONFIG"] = default_kube db_pw = state.inputs.get('init_password.db_password', '').strip() if db_pw: db_pw = inst_config._resolve_secret_value(db_pw) env["DB_PASSWORD"] = db_pw env["OPENTOFU_ADMIN_PASSWORD"] = db_pw grafana_pw = (state.config_data.get('Monitoring', {}) or {}).get('GRAFANA_ADMIN_PASSWORD', '').strip() resolved_grafana = inst_config._resolve_secret_value(grafana_pw) if grafana_pw else "" if resolved_grafana and not inst_config._is_openbao_ref(resolved_grafana) and not inst_config._is_prole_secret(resolved_grafana): env["GRAFANA_ADMIN_PASSWORD"] = resolved_grafana elif db_pw: env["GRAFANA_ADMIN_PASSWORD"] = db_pw db_user = (state.inputs.get('init_password.db_username', '') or '').strip() if db_user: env["PROLE_DB_USER"] = db_user # Kerberos env["KRB5_REALM"] = state.inputs.get('kerberos_config.realm', '') realm = (state.inputs.get('kerberos_config.realm', '') or '').strip() env["KRB5_KDC"] = state.inputs.get('kerberos_config.kdc', '') if realm: env["REALM"] = realm env["DOMAIN"] = realm.lower() env["KRB5_USER"] = state.inputs.get('kerberos_config.user', '') krb_pw = state.inputs.get('kerberos_config.password', '') if krb_pw: env["KRB5_PASSWORD"] = inst_config._resolve_secret_value(krb_pw) if state.inputs.get('kerberos_config.kdc', ''): env["KRB5_ADMIN"] = state.inputs.get('kerberos_config.kdc', '') kerberos_enabled = self._parse_bool(state.inputs.get('kerberos_config.enabled', 'False'), default=False) if kerberos_enabled is None: kerberos_enabled = False env["KERBEROS_ENABLED"] = "True" if kerberos_enabled else "False" env["ENABLED"] = env["KERBEROS_ENABLED"] at_rest = state.inputs.get('init_cluster.at_rest_encryption_enabled', '') if at_rest != '': env["AT_REST_ENCRYPTION_ENABLED"] = str(at_rest) argocd_ns = (state.config_data.get('Global', {}) or {}).get('ARGOCD_NAMESPACE', '').strip() if not argocd_ns: argocd_ns = (os.environ.get('ARGOCD_NAMESPACE') or '').strip() or 'argocd' env["ARGOCD_NAMESPACE"] = argocd_ns registry_ns = (state.config_data.get('Global', {}) or {}).get('REGISTRY_NAMESPACE', '').strip() if not registry_ns: registry_ns = (os.environ.get('REGISTRY_NAMESPACE') or '').strip() or 'default' env["REGISTRY_NAMESPACE"] = registry_ns if mode == 'k3s': server = state.inputs.get('init_cluster.k3s_server_url', '').strip() token = state.inputs.get('init_cluster.k3s_token', '').strip() if token: token = inst_config._resolve_secret_value(token) if server: if not server.startswith('http'): server = f"https://{server}" env["PROLE_K3S_SERVER"] = server if token: env["PROLE_K3S_TOKEN"] = token return env