chore: update prole.cfg defaults for dev cluster, refine config overrides

- Updated default paths, namespaces, and cluster names for k3d/dev configuration.
- Added `skip_names` set to exclude `prod.cfg` and `gcp.cfg` from override processing.
- Refined init parameters for Supabase, Kubernetes, and database deployments to align with dev-specific settings.
- Enhanced test coverage for excluded config files (`prod.cfg`, `gcp.cfg`) within `test_prole_conf.py`.
This commit is contained in:
chrisfu 2026-04-12 14:58:55 -07:00
parent 090b0e882a
commit cedcaae2c5
6 changed files with 376 additions and 13 deletions

View File

@ -268,6 +268,8 @@ PROLE_PROPS_PATH = PROLE_APP_DIR / "prole.properties"
import logging
LOGGER = logging.getLogger(__name__)
def setup_logging(verbose=False, debug=False):
"""Set up logging for the installer.
@ -998,7 +1000,25 @@ def _merge_kubeconfig(kubeconfig_path: str) -> bool:
return False
DEPENDENCIES = [
def _run_as_root_noninteractive(command: str) -> str:
return (
"if command -v sudo >/dev/null 2>&1; then "
f"sudo -n {command}; "
f"else {command}; fi"
)
def _apt_install(pkg: str) -> str:
return _run_as_root_noninteractive(
f"env DEBIAN_FRONTEND=noninteractive apt-get install -y {pkg}"
)
def _rpm_install(manager: str, pkg: str) -> str:
return _run_as_root_noninteractive(f"{manager} install -y {pkg}")
_MACOS_DEPENDENCIES = [
{
"id": "brew",
"name": "Homebrew",
@ -1090,6 +1110,218 @@ DEPENDENCIES = [
]
def _linux_dependencies(manager: str) -> list[dict]:
if manager not in {"apt", "dnf", "yum"}:
return []
install = _apt_install if manager == "apt" else lambda pkg: _rpm_install(manager, pkg)
return [
{
"id": "python",
"name": "python",
"description": "Python programming language",
"url": "https://www.python.org",
"install_cmd": install("python3"),
"check_cmd": "python3 --version",
"bin": "python3",
},
{
"id": "ansible",
"name": "ansible",
"description": "Infrastructure automation tool",
"url": "https://www.ansible.com",
"install_cmd": install("ansible"),
"check_cmd": "ansible --version",
"bin": "ansible",
},
{
"id": "kubectl",
"name": "kubectl",
"description": "Kubernetes command-line tool",
"url": "https://kubernetes.io/docs/reference/kubectl/",
"install_cmd": install("kubectl"),
"check_cmd": "kubectl version --client",
"bin": "kubectl",
},
{
"id": "kubectx",
"name": "kubectx",
"description": "Tool to switch between Kubernetes contexts",
"url": "https://github.com/ahmetb/kubectx",
"install_cmd": install("kubectx"),
"check_cmd": "kubectx -h",
"bin": "kubectx",
},
{
"id": "gcloud",
"name": "gcloud",
"description": "Google Cloud SDK CLI for GKE authentication and context setup",
"url": "https://cloud.google.com/sdk/docs/install",
"install_cmd": install("google-cloud-cli"),
"check_cmd": "gcloud --version",
"bin": "gcloud",
},
{
"id": "docker",
"name": "Docker",
"description": "Container platform for running Prole services",
"url": "https://www.docker.com",
"install_cmd": install("docker.io" if manager == "apt" else "docker"),
"check_cmd": "docker --version",
"bin": "docker",
},
{
"id": "k3d",
"name": "k3d",
"description": "Lightweight wrapper to run k3s in Docker",
"url": "https://k3d.io",
"install_cmd": install("k3d"),
"check_cmd": "k3d --version",
"bin": "k3d",
},
{
"id": "opentofu",
"name": "OpenTofu",
"description": "Open-source infrastructure as code tool",
"url": "https://opentofu.org",
"install_cmd": install("opentofu"),
"check_cmd": "tofu --version",
"bin": "tofu",
},
]
_DEP_PLATFORM_CACHE: Optional[dict] = None
def _detect_linux_package_manager() -> tuple[Optional[str], Optional[str]]:
if shutil.which("apt-get") and shutil.which("dpkg"):
return "apt", "apt/deb"
if shutil.which("dnf"):
return "dnf", "rpm"
if shutil.which("yum"):
return "yum", "rpm"
if shutil.which("rpm"):
return "rpm", "rpm"
return None, None
def detect_dependency_platform(refresh: bool = False) -> dict:
global _DEP_PLATFORM_CACHE
if _DEP_PLATFORM_CACHE is not None and not refresh:
return dict(_DEP_PLATFORM_CACHE)
os_raw = (platform.system() or "").lower()
arch = (platform.machine() or "unknown").lower()
info: dict[str, Optional[str] | bool] = {
"os": None,
"arch": arch,
"package_manager": None,
"package_family": None,
"supported": False,
"reason": None,
}
if os_raw == "darwin":
info.update(
{
"os": "macos",
"package_manager": "brew",
"package_family": "brew",
"supported": True,
}
)
elif os_raw == "linux":
manager, family = _detect_linux_package_manager()
info.update({"os": "linux", "package_manager": manager, "package_family": family})
if manager in {"apt", "dnf", "yum"}:
info["supported"] = True
elif manager == "rpm":
info["reason"] = (
"Linux RPM database detected but no supported package installer "
"(dnf/yum) was found."
)
elif shutil.which("brew"):
if _parse_bool(os.environ.get("PROLE_LINUX_ALLOW_BREW"), False):
info.update(
{
"package_manager": "brew",
"package_family": "brew",
"supported": True,
"reason": "Linux Homebrew explicitly enabled via PROLE_LINUX_ALLOW_BREW.",
}
)
else:
info["reason"] = (
"Linux Homebrew detected but native package managers (apt/dnf/yum) "
"were not found. Refusing brew fallback by default."
)
else:
info["reason"] = "No supported Linux package manager detected (apt/dnf/yum)."
else:
info["reason"] = f"Unsupported operating system: {platform.system() or 'unknown'}"
_DEP_PLATFORM_CACHE = dict(info)
return dict(info)
def get_dependency_milestone_title() -> str:
info = detect_dependency_platform()
if info.get("os") == "linux":
return "Linux Dependencies"
if info.get("os") == "macos":
return "Mac OS Dependencies"
return "Dependencies"
def get_dependency_platform_summary() -> str:
info = detect_dependency_platform()
return (
f"os={info.get('os') or 'unknown'}, "
f"arch={info.get('arch') or 'unknown'}, "
f"package_family={info.get('package_family') or 'unknown'}, "
f"package_manager={info.get('package_manager') or 'unknown'}"
)
def get_platform_dependencies() -> list[dict]:
info = detect_dependency_platform()
if not info.get("supported"):
return []
if info.get("os") == "macos":
return [dict(dep) for dep in _MACOS_DEPENDENCIES]
if info.get("os") == "linux":
manager = info.get("package_manager")
if manager == "brew":
return [dict(dep) for dep in _MACOS_DEPENDENCIES if dep["id"] != "brew"]
return _linux_dependencies(str(manager))
return []
def _validate_dependency_backend(dependencies: list[dict] | None = None) -> tuple[bool, str]:
info = detect_dependency_platform()
if not info.get("supported"):
reason = info.get("reason") or "Unsupported dependency backend"
return False, str(reason)
if info.get("os") == "linux":
deps = dependencies if dependencies is not None else get_platform_dependencies()
brew_refs = [dep.get("id") for dep in deps if "brew" in str(dep.get("install_cmd") or "")]
if brew_refs and info.get("package_manager") != "brew":
return (
False,
"Linux dependency set contains Homebrew commands while native package manager "
f"{info.get('package_manager')} is active: {', '.join([str(r) for r in brew_refs])}",
)
return True, ""
DEPENDENCIES = get_platform_dependencies()
def _resolve_brew_bin() -> str | None:
"""Return an absolute path to a `brew` binary if one is found in common locations."""
@ -1157,13 +1389,20 @@ def _augment_env_for_brew(env: dict | None = None) -> dict:
return env
def _augment_env_for_dependency_backend(env: dict | None = None) -> dict:
info = detect_dependency_platform()
if info.get("package_manager") == "brew":
return _augment_env_for_brew(env)
return env or os.environ.copy()
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:
env = _augment_env_for_brew(os.environ.copy())
env = _augment_env_for_dependency_backend(os.environ.copy())
if bin_name:
# Avoid `bash -l` here: login shell init scripts on some distros
# can overwrite PATH, defeating our augmented PATH for Linuxbrew.
@ -1222,7 +1461,8 @@ def load_dependencies(refresh: bool = True) -> list[dict]:
- location: Optional[str]
- version: Optional[str]
"""
deps: list[dict] = [dict(d) for d in DEPENDENCIES]
deps_source = DEPENDENCIES if DEPENDENCIES else get_platform_dependencies()
deps: list[dict] = [dict(d) for d in deps_source]
if refresh:
for dep in deps:
try:

View File

@ -4749,7 +4749,7 @@ class KnoeConsoleInstaller(KnoeInstaller):
if self._deployment_mode() != "k8s":
return True
gcloud_env = inst_config._augment_env_for_brew(os.environ.copy())
gcloud_env = inst_config._augment_env_for_dependency_backend(os.environ.copy())
cmd_base = ["gcloud"]
token_cmd = cmd_base + ["auth", "print-access-token", "--quiet"]
try:

View File

@ -43,16 +43,37 @@ def _stream_line(line: str) -> None:
class DependenciesMilestone(Milestone):
def __init__(self):
super().__init__("dependencies", "Dependency Verification")
super().__init__("dependencies", inst_config.get_dependency_milestone_title())
self.logger = logging.getLogger("DependenciesMilestone")
def execute(
self, state: InstallerState, progress: ProgressCallback | None = None
) -> None:
platform_info = inst_config.detect_dependency_platform()
self.logger.info(
"Dependency platform detected: %s",
inst_config.get_dependency_platform_summary(),
)
if progress:
progress("Checking dependencies...", 0.1)
dependencies = inst_config.DEPENDENCIES
if not dependencies:
dependencies = inst_config.get_platform_dependencies()
backend_ok, backend_reason = inst_config._validate_dependency_backend()
if not backend_ok:
self.logger.error("Dependency backend unsupported: %s", backend_reason)
self._set_status(state, "Missing")
return
if not dependencies:
reason = platform_info.get("reason") or "No dependencies configured for detected platform"
self.logger.error("Dependency resolution failed: %s", reason)
self._set_status(state, "Missing")
return
missing = []
for i, dep in enumerate(dependencies):
@ -130,7 +151,7 @@ class DependenciesMilestone(Milestone):
if cluster_env not in {"prod", "production", "k8s"}:
return True
gcloud_env = inst_config._augment_env_for_brew(os.environ.copy())
gcloud_env = inst_config._augment_env_for_dependency_backend(os.environ.copy())
cmd_base = ["gcloud"]
token_cmd = cmd_base + ["auth", "print-access-token", "--quiet"]
try:
@ -175,9 +196,11 @@ class DependenciesMilestone(Milestone):
return False
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
sections = ["Dependencies", inst_config.get_dependency_milestone_title()]
for section in sections:
if section not in state.config_data:
state.config_data[section] = {}
state.config_data[section]["STATUS"] = status
class NetworkScanMilestone(Milestone):

View File

@ -61,7 +61,7 @@ class Milestone(ABC):
proc_env = os.environ.copy()
if env:
proc_env.update(env)
proc_env = inst_config._augment_env_for_brew(proc_env)
proc_env = inst_config._augment_env_for_dependency_backend(proc_env)
proc = subprocess.Popen(
cmd,
stdout=subprocess.PIPE,

View File

@ -10,7 +10,6 @@ Covers:
from __future__ import annotations
import os
import subprocess
from pathlib import Path
from unittest.mock import MagicMock, patch
@ -22,6 +21,10 @@ from knoe.config import (
_extract_inline_vault_block,
_try_read_ansible_vault_value,
get_dep_info,
detect_dependency_platform,
get_dependency_milestone_title,
get_platform_dependencies,
_validate_dependency_backend,
load_dependencies,
get_ui_icon_image_path,
get_ui_background_image_path,
@ -296,6 +299,17 @@ def test_get_dep_info_prepends_linuxbrew_to_path_when_present():
return m
with patch.dict(os.environ, {"PATH": "/usr/bin:/bin"}, clear=False), \
patch(
"knoe.config.detect_dependency_platform",
return_value={
"os": "macos",
"arch": "arm64",
"package_manager": "brew",
"package_family": "brew",
"supported": True,
"reason": None,
},
), \
patch("knoe.config.os.path.isfile", side_effect=lambda p: p == "/home/linuxbrew/.linuxbrew/bin/brew"), \
patch("knoe.config.os.access", return_value=True), \
patch("knoe.config.os.path.isdir", side_effect=lambda p: p == "/home/linuxbrew/.linuxbrew/sbin"), \
@ -319,6 +333,92 @@ def test_get_dep_info_version_cmd_success():
assert ver == "Client Version: v1.28.0"
def test_detect_dependency_platform_linux_apt():
with patch("knoe.config._DEP_PLATFORM_CACHE", None), \
patch("knoe.config.platform.system", return_value="Linux"), \
patch("knoe.config.platform.machine", return_value="aarch64"), \
patch(
"knoe.config.shutil.which",
side_effect=lambda cmd: {
"apt-get": "/usr/bin/apt-get",
"dpkg": "/usr/bin/dpkg",
}.get(cmd),
):
info = detect_dependency_platform(refresh=True)
assert info["os"] == "linux"
assert info["package_family"] == "apt/deb"
assert info["package_manager"] == "apt"
assert info["supported"] is True
def test_detect_dependency_platform_linux_rejects_brew_fallback_by_default():
with patch("knoe.config._DEP_PLATFORM_CACHE", None), \
patch("knoe.config.platform.system", return_value="Linux"), \
patch("knoe.config.platform.machine", return_value="aarch64"), \
patch("knoe.config.shutil.which", side_effect=lambda cmd: "/home/linuxbrew/.linuxbrew/bin/brew" if cmd == "brew" else None), \
patch.dict(os.environ, {"PROLE_LINUX_ALLOW_BREW": ""}, clear=False):
info = detect_dependency_platform(refresh=True)
assert info["os"] == "linux"
assert info["supported"] is False
assert "Refusing brew fallback" in str(info["reason"])
def test_get_platform_dependencies_linux_apt_includes_opentofu_without_brew_dependency():
with patch(
"knoe.config.detect_dependency_platform",
return_value={
"os": "linux",
"arch": "aarch64",
"package_manager": "apt",
"package_family": "apt/deb",
"supported": True,
"reason": None,
},
):
deps = get_platform_dependencies()
ids = {d["id"] for d in deps}
assert "brew" not in ids
opentofu = next(d for d in deps if d["id"] == "opentofu")
assert "apt-get install -y opentofu" in str(opentofu.get("install_cmd") or "")
def test_validate_dependency_backend_rejects_linux_brew_commands():
with patch(
"knoe.config.detect_dependency_platform",
return_value={
"os": "linux",
"arch": "aarch64",
"package_manager": "apt",
"package_family": "apt/deb",
"supported": True,
"reason": None,
},
):
ok, reason = _validate_dependency_backend(
[{"id": "opentofu", "install_cmd": "brew install opentofu"}]
)
assert ok is False
assert "Homebrew commands" in reason
def test_get_dependency_milestone_title_linux_and_macos():
with patch(
"knoe.config.detect_dependency_platform",
return_value={"os": "linux"},
):
assert get_dependency_milestone_title() == "Linux Dependencies"
with patch(
"knoe.config.detect_dependency_platform",
return_value={"os": "macos"},
):
assert get_dependency_milestone_title() == "Mac OS Dependencies"
def test_get_dep_info_exception_returns_false():
dep = {"id": "broken", "name": "broken", "bin": "broken"}
with patch("knoe.config.shutil.which", side_effect=OSError("broken")):

View File

@ -665,7 +665,7 @@ def test_dependencies_k8s_fails_without_gcloud_auth_session():
with patch("knoe.config.get_dep_info", return_value=(True, "/usr/bin/gcloud", "1.0")), \
patch("knoe.config.DEPENDENCIES", [dep]), \
patch("knoe.config._augment_env_for_brew", return_value={"PATH": "x"}), \
patch("knoe.config._augment_env_for_dependency_backend", return_value={"PATH": "x"}), \
patch("knoe.config._gcloud_plain", side_effect=_unexpected_gcloud_plain), \
patch("knoe.core.milestones.subprocess.run", return_value=no_token), \
patch("knoe.core.milestones.sys.stdin.isatty", return_value=False):
@ -692,7 +692,7 @@ def test_dependencies_k8s_interactive_login_recovers_auth_session():
with patch("knoe.config.get_dep_info", return_value=(True, "/usr/bin/gcloud", "1.0")), \
patch("knoe.config.DEPENDENCIES", [dep]), \
patch("knoe.config._augment_env_for_brew", return_value={"PATH": "x"}), \
patch("knoe.config._augment_env_for_dependency_backend", return_value={"PATH": "x"}), \
patch("knoe.config._gcloud_plain", side_effect=_unexpected_gcloud_plain), \
patch("knoe.core.milestones.subprocess.run", side_effect=[no_token, with_token]), \
patch("knoe.core.milestones.sys.stdin.isatty", return_value=True), \