mirror of
https://github.com/dredx/prole.git
synced 2026-09-27 15:14:31 +00:00
- Deleted PostgREST Kubernetes deployment and service manifests. - Removed `init_postgrest.sh` initialization script and PostgREST references across UI, installer, and config. - Updated Kong API Gateway to exclude `/rest/*` routing to PostgREST. - Simplified `supabase/deploy.sh` by removing references to PostgREST scaling and alias creation. - Adjusted cluster configuration and UI to reflect the absence of PostgREST.
731 lines
31 KiB
Python
731 lines
31 KiB
Python
from __future__ import annotations
|
||
import logging
|
||
import os
|
||
import subprocess
|
||
import sys
|
||
import time
|
||
from pathlib import Path
|
||
from typing import TYPE_CHECKING
|
||
|
||
from installer.milestone import Milestone
|
||
from installer import config as inst_config
|
||
|
||
if TYPE_CHECKING:
|
||
from installer.state import InstallerState
|
||
from installer.milestone import ProgressCallback
|
||
|
||
|
||
def _stream_line(line: str) -> None:
|
||
try:
|
||
sys.stdout.write(line)
|
||
sys.stdout.flush()
|
||
except Exception:
|
||
pass
|
||
|
||
class DependenciesMilestone(Milestone):
|
||
def __init__(self):
|
||
super().__init__("dependencies", "Dependency Verification")
|
||
self.logger = logging.getLogger("DependenciesMilestone")
|
||
|
||
def execute(self, state: InstallerState, progress: ProgressCallback | None = None) -> None:
|
||
if progress:
|
||
progress("Checking dependencies...", 0.1)
|
||
|
||
dependencies = inst_config.DEPENDENCIES
|
||
missing = []
|
||
|
||
for i, dep in enumerate(dependencies):
|
||
ok, location, version = inst_config.get_dep_info(dep)
|
||
if ok:
|
||
self.logger.info(f"[OK] {dep['name']} {version or ''}".strip())
|
||
else:
|
||
missing.append(dep)
|
||
self.logger.info(f"[MISSING] {dep['name']}")
|
||
|
||
if progress:
|
||
progress(f"Checking {dep['name']}...", 0.1 + (i / len(dependencies)) * 0.4)
|
||
|
||
if not missing:
|
||
self._set_status(state, "All installed")
|
||
if progress:
|
||
progress("All dependencies installed", 1.0)
|
||
return
|
||
|
||
# Check if auto-install is enabled
|
||
auto_install = self._parse_bool(state.inputs.get('dependencies.auto_install_missing', 'True'))
|
||
if not auto_install:
|
||
self.logger.error("Dependencies missing and auto-install disabled.")
|
||
self._set_status(state, "Missing")
|
||
return
|
||
|
||
for i, dep in enumerate(missing):
|
||
install_cmd = dep.get('install_cmd')
|
||
if not install_cmd:
|
||
self.logger.error(f"No install command for {dep['name']}.")
|
||
continue
|
||
|
||
should_install = self._parse_bool(state.inputs.get(f"dependencies.{dep['id']}.install", 'True'))
|
||
if not should_install:
|
||
self.logger.info(f"[SKIP] {dep['name']} install disabled by config.")
|
||
continue
|
||
|
||
self.logger.info(f"[INSTALL] {dep['name']} -> {install_cmd}")
|
||
if progress:
|
||
progress(f"Installing {dep['name']}...", 0.5 + (i / len(missing)) * 0.4)
|
||
|
||
rc = self._run_cmd(install_cmd)
|
||
if rc != 0:
|
||
self.logger.error(f"Install failed for {dep['name']} (code {rc})")
|
||
|
||
# Re-verify
|
||
still_missing = []
|
||
for dep in missing:
|
||
ok, _, _ = inst_config.get_dep_info(dep)
|
||
if not ok:
|
||
still_missing.append(dep['name'])
|
||
|
||
if still_missing:
|
||
self.logger.error(f"Still missing: {', '.join(still_missing)}")
|
||
self._set_status(state, "Missing")
|
||
else:
|
||
self._set_status(state, "All installed")
|
||
if progress:
|
||
progress("All dependencies installed", 1.0)
|
||
|
||
def _set_status(self, state: InstallerState, status: str):
|
||
if 'Dependencies' not in state.config_data:
|
||
state.config_data['Dependencies'] = {}
|
||
state.config_data['Dependencies']['STATUS'] = status
|
||
|
||
|
||
class NetworkScanMilestone(Milestone):
|
||
def __init__(self):
|
||
super().__init__("network_scan", "Network Configuration")
|
||
self.logger = logging.getLogger("NetworkScanMilestone")
|
||
|
||
def execute(self, state: InstallerState, progress: ProgressCallback | None = None) -> None:
|
||
if not self._parse_bool(state.inputs.get('network_scan.run', 'True')):
|
||
self.logger.info("Network scan disabled.")
|
||
return
|
||
|
||
existing_kdc = state.config_data.get('Network', {}).get('KDC_AUTO_DETECTED')
|
||
if existing_kdc:
|
||
self.logger.info(f"Network scan already performed. KDC: {existing_kdc}")
|
||
return
|
||
|
||
scan_binary = inst_config.get_resource_path("prole-net/prole-agent")
|
||
if not scan_binary.exists():
|
||
self.logger.error(f"Scan binary not found at {scan_binary}")
|
||
return
|
||
|
||
prole_home = Path.home() / ".prole"
|
||
scan_dir = prole_home / "scan"
|
||
scan_dir.mkdir(parents=True, exist_ok=True)
|
||
|
||
kdc_found = None
|
||
|
||
def _handle_stdout(line: str):
|
||
nonlocal kdc_found
|
||
if "KDC is:" in line:
|
||
try:
|
||
ip_part = line.split("KDC is:")[1].strip()
|
||
ip = ip_part.split()[0].strip('[]():,')
|
||
if ip:
|
||
kdc_found = ip
|
||
except Exception:
|
||
pass
|
||
elif "Active Directory" in line or "88" in line:
|
||
import socket
|
||
for part in line.split():
|
||
try:
|
||
socket.inet_aton(part.strip('[]():,'))
|
||
kdc_found = part.strip('[]():,')
|
||
break
|
||
except Exception:
|
||
continue
|
||
|
||
if progress:
|
||
progress("Scanning network...", 0.5)
|
||
|
||
rc = self._run_cmd([str(scan_binary), "-t", "10"], cwd=str(scan_dir), on_stdout=_handle_stdout)
|
||
|
||
if kdc_found:
|
||
state.inputs['kerberos_config.kdc'] = kdc_found
|
||
state.inputs['kerberos_config.enabled'] = "True"
|
||
if 'Network' not in state.config_data:
|
||
state.config_data['Network'] = {}
|
||
state.config_data['Network']['KDC_AUTO_DETECTED'] = kdc_found
|
||
state.config_data['Network']['KERBEROS_AUTO_ENABLED'] = 'True'
|
||
self.logger.info(f"KDC detected: {kdc_found}")
|
||
|
||
if progress:
|
||
progress("Network scan complete", 1.0)
|
||
|
||
|
||
class EnvSetupMilestone(Milestone):
|
||
def __init__(self):
|
||
super().__init__("env_setup", "System Environment Setup")
|
||
self.logger = logging.getLogger("EnvSetupMilestone")
|
||
|
||
def execute(self, state: InstallerState, progress: ProgressCallback | None = None) -> None:
|
||
if progress:
|
||
progress("Setting up environment...", 0.5)
|
||
|
||
env_keys = ['PROLE_HOME', 'PROLE_CONF', 'PROLE_DATA', 'PROLE_LOGS', 'PROLE_SERVICE', 'NAMESPACE']
|
||
if 'System Environment' not in state.config_data:
|
||
state.config_data['System Environment'] = {}
|
||
|
||
for k in env_keys:
|
||
val = state.inputs.get(f'env_setup.{k}')
|
||
if val:
|
||
state.config_data['System Environment'][k] = val
|
||
|
||
if progress:
|
||
progress("Environment setup complete", 1.0)
|
||
|
||
|
||
class SecretManagementMilestone(Milestone):
|
||
def __init__(self):
|
||
super().__init__("secret_management", "Secret Resolution")
|
||
self.logger = logging.getLogger("SecretManagementMilestone")
|
||
|
||
def execute(self, state: InstallerState, progress: ProgressCallback | None = None) -> None:
|
||
if progress:
|
||
progress("Resolving secrets...", 0.5)
|
||
|
||
# Keys that might contain secrets
|
||
secret_keys = [
|
||
'init_password.db_password',
|
||
'init_password.db_password_confirm',
|
||
'kerberos_config.password',
|
||
'init_cluster.k3s_token'
|
||
]
|
||
|
||
for key in secret_keys:
|
||
val = state.inputs.get(key)
|
||
if val:
|
||
resolved = inst_config._resolve_secret_value(val)
|
||
if resolved != val:
|
||
state.inputs[key] = resolved
|
||
self.logger.info(f"Resolved secret for {key}")
|
||
|
||
# Synchronize confirm password
|
||
if state.inputs.get('init_password.db_password') and not state.inputs.get('init_password.db_password_confirm'):
|
||
state.inputs['init_password.db_password_confirm'] = state.inputs['init_password.db_password']
|
||
|
||
if progress:
|
||
progress("Secret resolution complete", 1.0)
|
||
|
||
|
||
class DatabaseCreationMilestone(Milestone):
|
||
def __init__(self):
|
||
super().__init__("init_password", "Database Creation")
|
||
self.logger = logging.getLogger("DatabaseCreationMilestone")
|
||
|
||
def execute(self, state: InstallerState, progress: ProgressCallback | None = None) -> None:
|
||
if progress:
|
||
progress("Preparing database configuration...", 0.5)
|
||
|
||
ns = (state.inputs.get('init_password.db_namespace', '') or '').strip()
|
||
if not ns:
|
||
ns = (state.inputs.get('env_setup.NAMESPACE', '') or '').strip() or 'default'
|
||
|
||
user = (state.inputs.get('init_password.db_username', '') or '').strip()
|
||
p1 = state.inputs.get('init_password.db_password', '')
|
||
|
||
# In ncurses/silent mode we might not have all fields yet, but we want to ensure defaults
|
||
if not user:
|
||
import getpass
|
||
user = getpass.getuser()
|
||
state.inputs['init_password.db_username'] = user
|
||
|
||
if not p1:
|
||
import secrets
|
||
import string
|
||
alphabet = string.ascii_letters + string.digits
|
||
p1 = ''.join(secrets.choice(alphabet) for i in range(24))
|
||
state.inputs['init_password.db_password'] = p1
|
||
state.inputs['init_password.db_password_confirm'] = p1
|
||
self.logger.info("Generated default database password.")
|
||
|
||
# If password is an unresolvable OpenBao or PROLE_SECRET reference
|
||
# (e.g. clean install where OpenBao hasn't been seeded yet),
|
||
# generate a fresh password so downstream scripts get a real value.
|
||
# OpenBao isn't running yet at this milestone stage, so if the ref
|
||
# can't be resolved right now we must provide a concrete password
|
||
# that will be seeded into OpenBao later by ClusterLifecycleMilestone.
|
||
if p1.startswith('${'):
|
||
resolved = inst_config._resolve_secret_value(p1)
|
||
if resolved and not resolved.startswith('${'):
|
||
p1 = resolved
|
||
else:
|
||
import secrets as _secrets
|
||
import string as _string
|
||
alphabet = _string.ascii_letters + _string.digits
|
||
p1 = ''.join(_secrets.choice(alphabet) for i in range(24))
|
||
self.logger.info("OpenBao/secret ref unresolvable at this stage; generated new DB password for initialization.")
|
||
state.inputs['init_password.db_password'] = p1
|
||
state.inputs['init_password.db_password_confirm'] = p1
|
||
|
||
if p1 and not p1.startswith('${') and not inst_config._is_prole_secret(p1):
|
||
if 'Inputs' not in state.config_data:
|
||
state.config_data['Inputs'] = {}
|
||
# Encrypt and save to config data for persistence in prole.cfg
|
||
encrypted_pw = inst_config._encrypt_cfg_secret(p1)
|
||
state.config_data['Inputs']['init_password.db_password'] = encrypted_pw
|
||
state.config_data['Inputs']['init_password.db_password_confirm'] = encrypted_pw
|
||
|
||
state.inputs['init_password.db_namespace'] = ns
|
||
state.inputs['env_setup.NAMESPACE'] = ns
|
||
|
||
if 'Database Creation' not in state.config_data:
|
||
state.config_data['Database Creation'] = {}
|
||
state.config_data['Database Creation']['DB_USER'] = user
|
||
state.config_data['Database Creation']['DB_NAME'] = ns
|
||
state.config_data['Database Creation']['NAMESPACE'] = ns
|
||
|
||
# SSH key generation (simplified for now)
|
||
if self._parse_bool(state.inputs.get('init_password.generate_ssh_key', 'True')):
|
||
key_path = Path.home() / ".ssh" / "id_prole_ed25519"
|
||
if not key_path.exists():
|
||
self._run_cmd(["ssh-keygen", "-t", "ed25519", "-N", "", "-f", str(key_path), "-C", user])
|
||
|
||
if progress:
|
||
progress("Database creation preparation complete", 1.0)
|
||
|
||
|
||
class DockerBuildMilestone(Milestone):
|
||
def __init__(self):
|
||
super().__init__("init_db_build", "Docker Build")
|
||
self.logger = logging.getLogger("DockerBuildMilestone")
|
||
|
||
def execute(self, state: InstallerState, progress: ProgressCallback | None = None) -> None:
|
||
if not self._parse_bool(state.inputs.get('init_db_build.run_build', 'True')):
|
||
self.logger.info("Docker build disabled.")
|
||
return
|
||
|
||
if progress:
|
||
progress("Building Prole database image...", 0.1)
|
||
|
||
tag = state.controller.get_prole_db_version()
|
||
from installer.core.env import _normalize_cluster_env
|
||
env_key = _normalize_cluster_env(state.inputs.get('init_cluster.cluster_env', 'dev')) or 'dev'
|
||
|
||
args = ["--tag", tag]
|
||
if env_key != 'dev':
|
||
# Add registry prefix for non-dev envs
|
||
server = state.inputs.get('init_cluster.k3s_server_url', 'myrddin.prole.org')
|
||
if '://' in server:
|
||
server = server.split('://')[1]
|
||
if ':' in server:
|
||
server = server.split(':')[0]
|
||
args = ["--tag", f"{server}:5000/prole-db:{tag}", "--push"]
|
||
|
||
rc = state.controller.run_script("build_db.sh", args=args, on_line=_stream_line)
|
||
if rc != 0:
|
||
self.logger.error(f"Docker build failed (code {rc})")
|
||
|
||
if progress:
|
||
progress("Docker build complete", 1.0)
|
||
|
||
|
||
class ClusterLifecycleMilestone(Milestone):
|
||
def __init__(self):
|
||
super().__init__("cluster_lifecycle", "Cluster Lifecycle Management")
|
||
self.logger = logging.getLogger("ClusterLifecycleMilestone")
|
||
|
||
def execute(self, state: InstallerState, progress: ProgressCallback | None = None) -> None:
|
||
from installer.core.env import _normalize_cluster_env
|
||
cluster_env = state.inputs.get('init_cluster.cluster_env', 'dev')
|
||
env_key = _normalize_cluster_env(cluster_env) or cluster_env
|
||
start_requested = self._parse_bool(state.inputs.get('init_cluster.start_cluster', 'True'))
|
||
|
||
if not start_requested:
|
||
self.logger.info("Cluster start not requested.")
|
||
return
|
||
|
||
env = self._get_script_env(state)
|
||
|
||
if env_key == 'dev':
|
||
if progress:
|
||
progress("Initializing k3d dev cluster...", 0.5)
|
||
# Ensure Docker is up (best-effort, macOS auto-start).
|
||
if not state.controller.check_docker_running():
|
||
if sys.platform == "darwin":
|
||
self.logger.info("Starting Docker...")
|
||
self._run_cmd(["open", "-a", "Docker"])
|
||
for _ in range(30):
|
||
time.sleep(2)
|
||
if state.controller.check_docker_running():
|
||
break
|
||
if not state.controller.check_docker_running():
|
||
self.logger.error("Docker is not running.")
|
||
return
|
||
|
||
cluster_name = "prole-dev-cluster"
|
||
res = subprocess.run(['k3d', 'cluster', 'list', '--no-headers'], capture_output=True, text=True)
|
||
res_stdout = res.stdout or ''
|
||
if cluster_name not in res_stdout:
|
||
from installer.core.env import _k3d_prole_data_volume_args
|
||
prole_data = state.inputs.get('env_setup.PROLE_DATA', str(inst_config.PROJECT_ROOT / "prole-db" / "data"))
|
||
volume_args = _k3d_prole_data_volume_args(prole_data)
|
||
cmd = ['k3d', 'cluster', 'create', cluster_name, '-a', '2'] + volume_args + ['--api-port', '0.0.0.0:6443']
|
||
rc = self._run_cmd(cmd, on_stdout=_stream_line)
|
||
if rc != 0:
|
||
self.logger.error(f"k3d cluster create failed (code {rc})")
|
||
else:
|
||
if 'running' not in res_stdout.lower():
|
||
self._run_cmd(['k3d', 'cluster', 'start', cluster_name], on_stdout=_stream_line)
|
||
|
||
try:
|
||
subprocess.run(['kubectl', 'config', 'use-context', f'k3d-{cluster_name}'], capture_output=True)
|
||
except Exception:
|
||
pass
|
||
|
||
# Ensure prole-db image is available in the cluster (avoid registry pull issues).
|
||
tag = state.controller.get_prole_db_version()
|
||
local_image = f"prole-db:{tag}"
|
||
if subprocess.run(['docker', 'image', 'inspect', local_image], capture_output=True).returncode == 0:
|
||
db_cfg = state.config_data.get('Docker Build', {}) or {}
|
||
registry = (db_cfg.get('LOCAL_REGISTRY_INTERNAL') or '').strip()
|
||
if registry.endswith(".localhost:5000"):
|
||
registry = registry.replace(".localhost:5000", ":5000")
|
||
elif registry.endswith(".localhost"):
|
||
registry = registry.replace(".localhost", "")
|
||
if not registry:
|
||
registry = "k3d-prole-registry:5000"
|
||
remote_tag = f"{registry}/prole-db:{tag}"
|
||
subprocess.run(['docker', 'tag', local_image, remote_tag], capture_output=True)
|
||
self._run_cmd(['k3d', 'image', 'import', remote_tag, '-c', cluster_name], on_stdout=_stream_line)
|
||
|
||
elif env_key in ('service', 'k3s'):
|
||
if progress:
|
||
progress("Verifying k3s connection...", 0.5)
|
||
|
||
# Additional logic for remote k3s services could go here
|
||
self.logger.info("Service cluster mode: verification and deployment triggered via scripts.")
|
||
# For now, matching the behavior of silent installer's _step_init_cluster
|
||
# which mostly sets up env and calls init scripts if needed.
|
||
|
||
# Initialize OpenBao once cluster is ready. If DB password is anchored,
|
||
# start OpenBao anyway so downstream scripts can attempt to read secrets.
|
||
db_pw_raw = (state.inputs.get('init_password.db_password', '') or '').strip()
|
||
if db_pw_raw:
|
||
db_pw = inst_config._resolve_secret_value(db_pw_raw)
|
||
if db_pw and not db_pw.startswith('${'):
|
||
env_init = dict(env)
|
||
env_init["DB_PASSWORD"] = db_pw
|
||
env_init.setdefault("GRAFANA_ADMIN_PASSWORD", db_pw)
|
||
db_user = (state.inputs.get('init_password.db_username', '') or '').strip()
|
||
if db_user:
|
||
env_init["PROLE_DB_USER"] = db_user
|
||
rc = state.controller.run_script(
|
||
"init_openbao.sh",
|
||
args=["initialize"],
|
||
env=env_init,
|
||
stdin_text=f"{db_pw}\n",
|
||
on_line=_stream_line,
|
||
)
|
||
if rc != 0:
|
||
self.logger.error(f"OpenBao initialization failed (code {rc})")
|
||
else:
|
||
self.logger.info("OpenBao init skipped: DB password unresolved/anchored. Ensuring OpenBao is running...")
|
||
rc = state.controller.run_script(
|
||
"init_openbao.sh",
|
||
args=["start"],
|
||
env=env,
|
||
on_line=_stream_line,
|
||
)
|
||
if rc != 0:
|
||
self.logger.error(f"OpenBao start failed (code {rc})")
|
||
|
||
# When common services are already healthy, run the service-layer
|
||
# migration inline so that the "Preparing Service Layer" steps are
|
||
# completed automatically — mirroring the UI shortcut behaviour.
|
||
if env_key in ('service', 'k3s'):
|
||
service_ns = env.get("SERVICE_NAMESPACE", env.get("NAMESPACE", "default"))
|
||
status_args = ["-n", service_ns]
|
||
if self._parse_bool(state.inputs.get('init_cluster.kerberos_enabled', 'False')):
|
||
status_args.append("-k")
|
||
rc_status = state.controller.run_script(
|
||
"status_common_services.sh",
|
||
args=status_args,
|
||
env=env,
|
||
on_line=_stream_line,
|
||
)
|
||
if rc_status == 0:
|
||
self.logger.info("All common services healthy – running service layer migration inline.")
|
||
rc_migrate = state.controller.run_script(
|
||
"init_service_layer.sh",
|
||
args=["migrate"],
|
||
env=env,
|
||
on_line=_stream_line,
|
||
)
|
||
if rc_migrate != 0:
|
||
self.logger.error(f"Service layer migration failed (code {rc_migrate})")
|
||
else:
|
||
self.logger.info("Service layer migration completed successfully.")
|
||
|
||
if progress:
|
||
progress("Cluster lifecycle management complete", 1.0)
|
||
|
||
|
||
class InitializationScriptsMilestone(Milestone):
|
||
def __init__(self):
|
||
super().__init__("init_scripts", "Initialization Scripts")
|
||
self.logger = logging.getLogger("InitializationScriptsMilestone")
|
||
|
||
def _regenerate_port_mapping_cfg(self, state: InstallerState, mode: str) -> None:
|
||
"""Regenerate conf/port-mapping.cfg from [Port Forwards] in prole.cfg data."""
|
||
conf_dir = inst_config.PROJECT_ROOT / 'conf'
|
||
mapping_path = conf_dir / 'port-mapping.cfg'
|
||
pf_section = state.config_data.get('Port Forwards', {}) or {}
|
||
prefix = "PORT_FORWARD_K3S_MAPPING_" if mode == "k3s" else "PORT_FORWARD_K3D_MAPPING_"
|
||
|
||
# Resolve ${NAMESPACE} from state
|
||
ns = (state.inputs.get('init_password.db_namespace', '') or '').strip()
|
||
if not ns:
|
||
ns = (state.config_data.get('Global', {}) or {}).get('NAMESPACE', '').strip()
|
||
if not ns:
|
||
ns = 'default'
|
||
|
||
lines = [
|
||
"# Port mappings for Prole Tools (generated).",
|
||
"# Format: key: local=... remote=... ns=... svc=... address=...",
|
||
""
|
||
]
|
||
for k, v in sorted(pf_section.items()):
|
||
if not k.startswith(prefix):
|
||
continue
|
||
parts = {}
|
||
for p in str(v).split(';'):
|
||
if '=' in p:
|
||
kv = p.split('=', 1)
|
||
if len(kv) == 2:
|
||
parts[kv[0].strip()] = kv[1].strip()
|
||
if 'id' not in parts:
|
||
continue
|
||
m_id = parts['id']
|
||
local = parts.get('hostPort', '')
|
||
remote = parts.get('servicePort', '')
|
||
m_ns = parts.get('namespace', '')
|
||
target = parts.get('target', '')
|
||
addr = parts.get('address', '0.0.0.0')
|
||
# Resolve ${NAMESPACE} references
|
||
if m_ns.startswith('${') and 'NAMESPACE' in m_ns:
|
||
m_ns = ns
|
||
if local.startswith('${'):
|
||
continue # skip entries with unresolvable port refs
|
||
svc = target[4:] if target.startswith('svc/') else target
|
||
lines.append(f"{m_id}: local={local} remote={remote} ns={m_ns} svc={svc} address={addr}")
|
||
|
||
try:
|
||
mapping_path.write_text('\n'.join(lines) + '\n')
|
||
self.logger.info(f"Regenerated {mapping_path}")
|
||
except Exception as e:
|
||
self.logger.error(f"Failed to regenerate port-mapping.cfg: {e}")
|
||
|
||
def execute(self, state: InstallerState, progress: ProgressCallback | None = None) -> None:
|
||
env = self._get_script_env(state)
|
||
mode = env.get("PROLE_MODE", "")
|
||
mode_args = ["--mode", mode] if mode else []
|
||
|
||
# Regenerate port-mapping.cfg from prole.cfg data to ensure it's clean
|
||
self._regenerate_port_mapping_cfg(state, mode or 'k3d')
|
||
|
||
scripts = ["init_common_services.sh", "init_cloudnative_pg.sh"]
|
||
|
||
# Optional scripts
|
||
if self._parse_bool(state.inputs.get('kerberos_config.enabled', 'False')):
|
||
scripts.append("init_kerberos.sh")
|
||
scripts.extend([
|
||
"init_prole-db-backup.sh",
|
||
"init_kong.sh",
|
||
"init_db_manager.sh",
|
||
"init_monitoring.sh",
|
||
])
|
||
if mode != "k3d":
|
||
scripts.append("init_nginx_ingress.sh")
|
||
|
||
for i, script in enumerate(scripts):
|
||
if progress:
|
||
progress(f"Running {script}...", (i / len(scripts)))
|
||
|
||
# Align args with legacy silent installer behavior
|
||
args = ["update"]
|
||
if script == "init_common_services.sh":
|
||
args = mode_args + ["-n", env.get("SERVICE_NAMESPACE", env.get("NAMESPACE", "default"))]
|
||
if self._parse_bool(state.inputs.get('kerberos_config.enabled', 'False')):
|
||
args.append("-k")
|
||
args.append("update")
|
||
elif script == "init_cloudnative_pg.sh":
|
||
args = mode_args + ["initialize"]
|
||
elif script == "init_kerberos.sh":
|
||
args = mode_args + ["initialize"]
|
||
elif script == "init_prole-db-backup.sh":
|
||
args = mode_args + ["start"]
|
||
elif script in ("init_kong.sh", "init_db_manager.sh"):
|
||
args = mode_args + ["start"]
|
||
elif script in ("init_monitoring.sh", "init_nginx_ingress.sh"):
|
||
args = mode_args + ["initialize"]
|
||
|
||
rc = state.controller.run_script(script, args=args, env=env, on_line=_stream_line)
|
||
if rc != 0:
|
||
msg = f"Script {script} failed (code {rc})"
|
||
self.logger.error(msg)
|
||
raise Exception(msg)
|
||
|
||
# Verify critical secrets
|
||
ns = env.get("NAMESPACE", "default")
|
||
self.logger.info(f"Verifying critical secrets in namespace {ns}...")
|
||
critical_secrets = ["prole-db-user", "prole-db-superuser", "cnpg-admin-key"]
|
||
missing_secrets = []
|
||
for secret in critical_secrets:
|
||
if self._run_cmd(f"kubectl get secret {secret} -n {ns}") != 0:
|
||
missing_secrets.append(secret)
|
||
|
||
if missing_secrets:
|
||
msg = f"CRITICAL: Missing secrets in namespace '{ns}': {', '.join(missing_secrets)}. Database initialization will fail."
|
||
self.logger.error(msg)
|
||
if progress:
|
||
progress(msg, 1.0)
|
||
raise Exception(msg)
|
||
|
||
# For k3d: start port-forward daemon and verify Grafana port-forward.
|
||
pf_ok = True
|
||
if mode == "k3d":
|
||
def _pf_line(line: str) -> None:
|
||
self.logger.info(line.rstrip('\n'))
|
||
|
||
env_pf = dict(env)
|
||
env_pf.setdefault("PORT_FORWARD_SKIP_VALIDATE", "1")
|
||
env_pf.setdefault("PORT_FORWARD_SKIP_WAIT", "1")
|
||
env_pf.setdefault("PORT_FORWARD_WAIT_TIMEOUT", "20")
|
||
env_pf.setdefault("PORT_FORWARD_WAIT_INTERVAL", "2")
|
||
|
||
rc_pf = state.controller.run_script(
|
||
"init_port_forwards.sh",
|
||
args=["--force", "--mode", mode, "start"],
|
||
env=env_pf,
|
||
on_line=_stream_line,
|
||
)
|
||
if rc_pf != 0:
|
||
pf_ok = False
|
||
self.logger.error(f"Port-forwards start failed (code {rc_pf})")
|
||
else:
|
||
for _ in range(10):
|
||
status_lines: list[str] = []
|
||
|
||
def _pf_status_line(line: str) -> None:
|
||
status_lines.append(line)
|
||
_stream_line(line)
|
||
|
||
rc_status = state.controller.run_script(
|
||
"init_port_forwards.sh",
|
||
args=["--mode", mode, "status", "grafana"],
|
||
env=env_pf,
|
||
on_line=_pf_status_line,
|
||
)
|
||
running = any(
|
||
l.lstrip().startswith("RUNNING") and "grafana" in l for l in status_lines
|
||
)
|
||
if rc_status == 0 and running:
|
||
break
|
||
time.sleep(1)
|
||
else:
|
||
pf_ok = False
|
||
self.logger.error("Grafana port-forward not running after start.")
|
||
|
||
pf_section = state.config_data.setdefault("Port Forwards", {})
|
||
pf_section["STATUS"] = "Running" if pf_ok else "Failed"
|
||
|
||
if progress:
|
||
progress("Initialization scripts complete", 1.0)
|
||
|
||
|
||
class DeploymentMilestone(Milestone):
|
||
def __init__(self):
|
||
super().__init__("deployment", "Prole Deployment")
|
||
self.logger = logging.getLogger("DeploymentMilestone")
|
||
|
||
def execute(self, state: InstallerState, progress: ProgressCallback | None = None) -> None:
|
||
if progress:
|
||
progress("Deploying Prole...", 0.3)
|
||
|
||
env = self._get_script_env(state)
|
||
mode = env.get("PROLE_MODE", "")
|
||
mode_args = ["--mode", mode] if mode else []
|
||
# Using init_cloudnative_pg.sh as the primary deployment mechanism
|
||
rc = state.controller.run_script(
|
||
"init_cloudnative_pg.sh",
|
||
args=mode_args + ["deploy", "latest"],
|
||
env=env,
|
||
on_line=_stream_line,
|
||
)
|
||
|
||
if rc == 0:
|
||
self.logger.info("Deployment successful")
|
||
else:
|
||
msg = f"Deployment failed (code {rc})"
|
||
self.logger.error(msg)
|
||
raise Exception(msg)
|
||
|
||
# Deploy Supabase if enabled
|
||
supabase_enabled = self._parse_bool(
|
||
state.inputs.get('init_cluster.supabase_enabled', 'False'),
|
||
default=False,
|
||
)
|
||
if supabase_enabled:
|
||
if progress:
|
||
progress("Deploying Supabase...", 0.6)
|
||
self._deploy_supabase(state, env, mode)
|
||
|
||
if progress:
|
||
progress("Deployment complete", 1.0)
|
||
|
||
def _deploy_supabase(self, state: InstallerState, env: dict, mode: str) -> None:
|
||
from installer.core.env import _normalize_cluster_env, _parse_bool
|
||
|
||
script_path = state.controller.project_root / "supabase" / "deploy.sh"
|
||
if not script_path.exists():
|
||
self.logger.error(f"Supabase deploy script not found: {script_path}")
|
||
state.config_data.setdefault('Supabase', {})['STATUS'] = 'Attempted'
|
||
return
|
||
|
||
deploy_mode = (
|
||
os.environ.get("SUPABASE_DEPLOY_MODE")
|
||
or os.environ.get("SUPABASE_MODE")
|
||
or ""
|
||
).strip()
|
||
if not deploy_mode:
|
||
cluster_env = _normalize_cluster_env(
|
||
state.inputs.get('init_cluster.cluster_env', 'dev')
|
||
)
|
||
if cluster_env in ('service', 'prod'):
|
||
deploy_mode = "k8s"
|
||
else:
|
||
deploy_mode = "k3d"
|
||
|
||
args = ["--mode", deploy_mode]
|
||
cfg_path = state.controller.project_root / "conf" / "prole.cfg"
|
||
if cfg_path.exists():
|
||
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.logger.info(f"supabase/deploy.sh {' '.join(args)}")
|
||
rc = subprocess.run(
|
||
["bash", str(script_path)] + args,
|
||
env=env,
|
||
).returncode
|
||
if rc == 0:
|
||
self.logger.info("Supabase deployment successful")
|
||
state.config_data.setdefault('Supabase', {})['STATUS'] = 'Deployed'
|
||
else:
|
||
msg = f"Supabase deploy failed (code {rc})"
|
||
self.logger.error(msg)
|
||
state.config_data.setdefault('Supabase', {})['STATUS'] = 'Attempted'
|
||
raise Exception(msg)
|