prole/installer/config.py

275 lines
8.6 KiB
Python

"""
Shared configuration and utility functions for the Prole installer (root-level).
This mirrors `prole.installer.config` but is located under the root `installer/`
package per the refactor request. UI code should import from `installer.config`.
"""
from __future__ import annotations
from pathlib import Path
import platform
import subprocess
import sys
from typing import Tuple, Optional
import os
# .properties loader (simple key=value, # comments)
def _load_properties(path: Path) -> dict:
props: dict[str, str] = {}
try:
text = path.read_text(encoding="utf-8")
except Exception:
return props
for line in text.splitlines():
s = line.strip()
if not s or s.startswith("#"):
continue
if "=" in s:
k, v = s.split("=", 1)
k = k.strip()
v = v.strip()
if k:
props[k] = v
return props
# Repository root: this file lives at <repo>/installer/config.py → parent is repo
PROJECT_ROOT = Path(__file__).resolve().parents[1]
PROLE_APP_DIR = PROJECT_ROOT / "prole-app"
PROLE_PROPS_PATH = PROLE_APP_DIR / "prole.properties"
import logging
def setup_logging(verbose=False, debug=False):
"""Set up logging for the installer."""
level = logging.INFO
if debug:
level = logging.DEBUG
elif verbose:
level = logging.INFO # INFO is already default, but we can be more explicit if needed
# We use a custom format that's clean but informative
log_format = '%(asctime)s [%(levelname)s] %(name)s: %(message)s'
handlers: list[logging.Handler] = [logging.StreamHandler()]
if debug:
# Debug logging to file
log_dir = PROJECT_ROOT / "logs"
log_dir.mkdir(parents=True, exist_ok=True)
from datetime import datetime
ts = datetime.now().strftime("%Y%m%d-%H%M%S")
debug_log = log_dir / f"install-debug-{ts}.log"
handlers.append(logging.FileHandler(debug_log))
# Use more verbose format for file
file_formatter = logging.Formatter(log_format)
handlers[-1].setFormatter(file_formatter)
print(f"Debug logging enabled to {debug_log}")
logging.basicConfig(
level=level,
format=log_format,
handlers=handlers,
force=True # Override any existing config
)
_PROLE_PROPS_CACHE: Optional[dict] = None
def get_properties() -> dict:
"""Load and cache prole.properties from the repo (installer context).
Order:
- Repo default at prole-app/prole.properties
- Optional env override: PROLE_PROPERTIES points to a file
"""
global _PROLE_PROPS_CACHE
if _PROLE_PROPS_CACHE is None:
props: dict[str, str] = {}
# repo default
if PROLE_PROPS_PATH.exists():
props.update(_load_properties(PROLE_PROPS_PATH))
# env override (absolute path)
env_path = os.environ.get("PROLE_PROPERTIES")
if env_path:
p = Path(env_path)
if p.exists():
props.update(_load_properties(p))
_PROLE_PROPS_CACHE = props
return dict(_PROLE_PROPS_CACHE)
def get_config_value(key: str, default: Optional[str] = None) -> Optional[str]:
return get_properties().get(key, default)
def get_ui_icon_image_path() -> Path:
"""Return absolute path to the UI icon image for the installer.
Priority (new → legacy):
- `icon` key in prole.properties (requested)
- legacy `ui.icon`
Defaults to img/proleIcon.png under repo root if not set or missing.
"""
# Prefer new key `icon`, fall back to old `ui.icon`
rel = (
get_config_value("icon")
or get_config_value("ui.icon")
or "img/proleIcon.png"
)
p = (PROJECT_ROOT / rel).resolve()
if p.exists():
return p
# fallback
return (PROJECT_ROOT / "img/proleIcon.png").resolve()
def get_ui_background_image_path() -> Path:
"""Return absolute path to the UI background image.
Priority (new → legacy):
- `background` key in prole.properties (requested)
- legacy `ui.background`
Defaults to img/proleLogoSepia.png under repo root if not set or missing.
"""
rel = (
get_config_value("background")
or get_config_value("ui.background")
or "img/proleLogoSepia.png"
)
p = (PROJECT_ROOT / rel).resolve()
if p.exists():
return p
# fallback
return (PROJECT_ROOT / "img/proleLogoSepia.png").resolve()
def is_apple_silicon() -> bool:
return platform.machine() == "arm64" and platform.system() == "Darwin"
def get_docker_build_platform_args(target_env: Optional[str] = None) -> list[str]:
override = os.environ.get("PROLE_DOCKER_PLATFORM", "").strip()
if override:
return ["--platform", override]
env_key = (target_env or "").strip().lower()
if env_key == "dev" and is_apple_silicon():
return ["--platform", "linux/arm64"]
if env_key in ("service", "k3s", "prole-service-cluster"):
return ["--platform", "linux/arm64"]
if is_apple_silicon():
return ["--platform", "linux/amd64"]
return []
def normalize_version(text: str) -> str:
if not text:
return ""
import re
m = re.search(r"(\d+(?:[._-]\d+){0,5})", text)
if not m:
nums = re.findall(r"\d+", text)
else:
nums = re.findall(r"\d+", m.group(1))
if not nums:
return text.strip()
parts = (nums + ["0", "0"])[:3]
try:
parts = [f"{int(p):02d}" for p in parts]
except Exception:
parts = [p.zfill(2) for p in parts]
return ".".join(parts)
DEPENDENCIES = [
{
"id": "brew",
"name": "Homebrew",
"description": "Package manager for macOS",
"url": "https://brew.sh",
"install_cmd": '/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"',
"check_cmd": "brew --version",
"bin": "brew",
},
{
"id": "python",
"name": "python",
"parent": "brew",
"description": "Python programming language",
"url": "https://www.python.org",
"install_cmd": "brew install python",
"check_cmd": "python3 --version",
"bin": "python3",
},
{
"id": "ansible",
"name": "ansible",
"parent": "brew",
"description": "Infrastructure automation tool",
"url": "https://www.ansible.com",
"install_cmd": "brew install ansible",
"check_cmd": "ansible --version",
"bin": "ansible",
},
{
"id": "kubectl",
"name": "kubectl",
"parent": "brew",
"description": "Kubernetes command-line tool",
"url": "https://kubernetes.io/docs/reference/kubectl/",
"install_cmd": "brew install kubectl",
"check_cmd": "kubectl version --client",
"bin": "kubectl",
},
{
"id": "kubectl_cnpg",
"name": "kubectl-cnpg",
"parent": "brew",
"description": "CloudNative-PG kubectl plugin",
"url": "https://cloudnative-pg.io/",
"install_cmd": "brew install kubectl-cnpg",
"check_cmd": "kubectl cnpg version",
"bin": "kubectl-cnpg",
},
{
"id": "docker",
"name": "Docker",
"description": "Container platform for running Prole services",
"url": "https://www.docker.com/products/docker-desktop",
"install_cmd": None,
"check_cmd": "docker --version",
"bin": "docker",
},
{
"id": "k3d",
"name": "k3d",
"description": "Lightweight wrapper to run k3s in Docker",
"url": "https://k3d.io",
"install_cmd": "brew install k3d",
"check_cmd": "k3d --version",
"bin": "k3d",
},
]
def get_dep_info(dep: dict) -> Tuple[bool, Optional[str], Optional[str]]:
bin_name = dep.get("bin") or dep["name"]
location = None
version = None
installed = False
try:
if bin_name:
res = subprocess.run(["bash", "-lc", f"command -v {bin_name}"], capture_output=True, text=True)
if res.returncode == 0:
location = res.stdout.strip()
installed = True
check_cmd = dep.get("check_cmd")
if check_cmd:
res2 = subprocess.run(["bash", "-lc", check_cmd], capture_output=True, text=True)
if res2.returncode == 0:
version = " ".join(res2.stdout.strip().splitlines()[:1])
# If check_cmd succeeded, consider it installed
installed = True
if not location:
location = "Installed via Python"
except Exception:
pass
return installed, location, version