From c24ca728dd64793afe40575920c2a3890b17b57e Mon Sep 17 00:00:00 2001 From: chrisfu Date: Sun, 8 Mar 2026 22:09:00 -0700 Subject: [PATCH] Remove obsolete configurations and improve dependency handling - Deleted `prole.cfg`, `port-mapping.cfg`, and PostgreSQL version file as part of configuration cleanup. - Enhanced Homebrew dependency resolution with `_resolve_brew_bin` and `_augment_env_for_brew` methods to manage PATH automatically. - Updated subprocess calls to respect augmented environment configurations. - Added unit tests for Homebrew PATH augmentation and dependency validation logic. --- conf/port-mapping.cfg | 8 -- conf/postgresql/.version | 1 - conf/prole.cfg | 187 ------------------------- installer/config.py | 75 +++++++++- installer/milestone.py | 3 + tests/installer/test_config_helpers.py | 36 +++++ 6 files changed, 111 insertions(+), 199 deletions(-) delete mode 100644 conf/postgresql/.version delete mode 100644 conf/prole.cfg diff --git a/conf/port-mapping.cfg b/conf/port-mapping.cfg index 94edb47..986985d 100644 --- a/conf/port-mapping.cfg +++ b/conf/port-mapping.cfg @@ -1,11 +1,3 @@ # Port mappings for Prole Tools (generated). # Format: key: local=... remote=... ns=... svc=... address=... -argocd: local=8081 remote=80 ns=argocd svc=argocd-server address=0.0.0.0 -garage: local=3900 remote=3900 ns=knoe-system svc=garage address=0.0.0.0 -openbao: local=8200 remote=8200 ns=knoe-system svc=openbao address=0.0.0.0 -opentofu: local=8080 remote=8080 ns=knoe-system svc=opentofu address=0.0.0.0 -dashboard: local=8443 remote=443 ns=kubernetes-dashboard svc=kubernetes-dashboard-kong-proxy address=127.0.0.1 -postgres: local=5432 remote=5432 ns=prole-db-chrisfu-4ac8c0 svc=prole-db-rw address=0.0.0.0 -prometheus: local=9090 remote=9090 ns=monitoring svc=kps-kube-prometheus-stack-prometheus address=127.0.0.1 -grafana: local=3000 remote=80 ns=monitoring svc=kps-grafana address=0.0.0.0 diff --git a/conf/postgresql/.version b/conf/postgresql/.version deleted file mode 100644 index 25bf17f..0000000 --- a/conf/postgresql/.version +++ /dev/null @@ -1 +0,0 @@ -18 \ No newline at end of file diff --git a/conf/prole.cfg b/conf/prole.cfg deleted file mode 100644 index da80b8c..0000000 --- a/conf/prole.cfg +++ /dev/null @@ -1,187 +0,0 @@ -; Prole Master Configuration File -; Generated by install.py on 2026-03-07 23:56:48 -; This file is used as input for Ansible deployment and k8s cluster creation. - -[User] -; User-editable values; derived values below reference these by default. -NAMESPACE = knoe-db -PROLE_CONF = ${PROLE_HOME}/conf -PROLE_DATA = /Users/chrisfu/.prole/data -PROLE_HOME = /Users/chrisfu/dev/prole -PROLE_LOGS = /opt/prole/logs/chrisfu -PROLE_SERVICE = ${PROLE_HOME}/etc -SERVICE_NAMESPACE = knoe-system - -[Inputs] -; Screen-scoped inputs used for unattended replays (-S) -build.deploy_env = Dev -build.run_build = false -database_options.distribution = percona -database_options.ext.dblink = true -database_options.ext.pg_buffercache = true -database_options.ext.pg_freespacemap = true -database_options.ext.pg_repack = true -database_options.ext.pg_stat_monitor = true -database_options.ext.pg_stat_statements = true -database_options.ext.pgaudit = true -database_options.ext.pgbadger = true -database_options.ext.pgcrypto = true -database_options.ext.pgrowlocks = true -database_options.ext.pgvector = true -database_options.ext.postgis = true -database_options.ext.postgres_fdw = true -database_options.version_type = v18 -dependencies.ansible.install = true -dependencies.auto_install_missing = true -dependencies.brew.install = true -dependencies.docker.install = true -dependencies.k3d.install = true -dependencies.kubectl.install = true -dependencies.kubectx.install = true -dependencies.opentofu.install = true -dependencies.python.install = true -dependencies.verify_all = false -disk_selection.disk_type = local -disk_selection.local_path = ${PROLE_HOME}/prole-tools-app/dist -disk_selection.removable_mount = -env_setup.NAMESPACE = ${NAMESPACE} -env_setup.PROLE_CONF = ${PROLE_CONF} -env_setup.PROLE_DATA = ${PROLE_DATA} -env_setup.PROLE_HOME = ${PROLE_HOME} -env_setup.PROLE_LOGS = ${PROLE_LOGS} -env_setup.PROLE_SERVICE = ${PROLE_SERVICE} -init_cluster.at_rest_encryption_enabled = true -init_cluster.cluster_env = service -init_cluster.deployment_target = prole-service-cluster -init_cluster.gitops_enabled = false -init_cluster.k3s_server_url = https://myrddin.prole.org:6443 -init_cluster.k3s_token = ${PROLE_SECRET:v1:i1FXxxDV0iTGUprA:Esd5SwMPbXNiWB1FeAPJqDVMhaIwg6TeR7aPH-IbF3HURxH6qs89IPQWhAAOmuFYLNCG_Fc8e7y6knoe-dbTbIrV-md-fOVOZ8PbRHwGp_6u2w7O5WUf_QIdhxF30H-UQZlFZHOAHnW2l970O2zQQPGRJknoe-dbqQq6VzoK2_P6tiJ8uA==} -init_cluster.kerberos_enabled = true -init_cluster.mode = k3s -init_cluster.start_cluster = true -init_cluster.supabase_enabled = false -init_cnpg_deploy.force_rollout = false -init_cnpg_deploy.run_deploy = true -init_db_build.run_build = true -init_password.db_host_port = 5432 -init_password.db_namespace = ${NAMESPACE} -init_password.db_password = ${PROLE_SECRET:v1:eRSi-Qb0odxbdYGH:KedZCSfHSVX6cmxyqrZG2VwA6T0aH-n1m0hSZcpCJ3T-pobLBKtb} -init_password.db_password_confirm = ${PROLE_SECRET:v1:eRSi-Qb0odxbdYGH:KedZCSfHSVX6cmxyqrZG2VwA6T0aH-n1m0hSZcpCJ3T-pobLBKtb} -init_password.db_username = root -init_password.generate_ssh_key = true -init_scripts.run_scripts = true -kerberos_config.enabled = true -kerberos_config.kdc = 10.0.0.3 -kerberos_config.password = ${PROLE_SECRET:v1:yXgcBJEzbAwNJ9E_:Aq6ppY9xJzN22l2FYAdVWWHyuAssQdyZKZMYm_m2SDgT6peM} -kerberos_config.realm = PROLE.ORG -kerberos_config.test_connection = false -kerberos_config.user = administrator -network_scan.run = true -ollama_config.model = -ollama_config.server_host = -ollama_config.server_port = 11434 - -[Global] -; Variables used by name in more than one place or assumed global scope -CLUSTER_ENV = service -DB_HOST_PORT = 5432 -DB_PASSWORD = ${PROLE_SECRET:v1:JSPYBCmth7xOrNlE:dH6t-0yLoc5qvt-sBFoQw7KRWo84LXPgUU4EIpGIRJyBMcTVFcjC} -DEPLOYMENT_MODE = k3s -DEPLOYMENT_TARGET = prole-service-cluster -DOCKER_IMPORT_DIR = -NAMESPACE = ${NAMESPACE} -PROLE_DB_USER = root -PROLE_HOME = ${PROLE_HOME} -PROLE_K3S_SERVER = https://myrddin.prole.org:6443 -PROLE_K3S_TOKEN = ${PROLE_SECRET:v1:i1FXxxDV0iTGUprA:Esd5SwMPbXNiWB1FeAPJqDVMhaIwg6TeR7aPH-IbF3HURxH6qs89IPQWhAAOmuFYLNCG_Fc8e7y6knoe-dbTbIrV-md-fOVOZ8PbRHwGp_6u2w7O5WUf_QIdhxF30H-UQZlFZHOAHnW2l970O2zQQPGRJknoe-dbqQq6VzoK2_P6tiJ8uA==} -PROLE_OPENTOFU_URL = http://127.0.0.1:8080 -SERVICE_NAMESPACE = knoe-system - -[Welcome] -; No configuration values captured yet for this section. - -[Dependencies] -; No configuration values captured yet for this section. - -[Network] -AD_DC_HOST = myrddin.prole.org -AD_DC_IP = 10.0.0.3 -ANSIBLE_DOMAIN = prole.org -ANSIBLE_INFRASTRUCTURE = ${PROLE_HOME}/infrastructure -ANSIBLE_INVENTORY = ${PROLE_HOME}/infrastructure/inventory -ANSIBLE_REALM = PROLE.ORG -ANSIBLE_TOPOLOGY = {"domain":"prole.org","realm":"PROLE.ORG","internal_records":{"aventage.prole.org":"10.0.0.206","fairyland.prole.org":"10.0.0.208","loghost.prole.org":"10.0.0.3","merlin.prole.org":"10.0.0.6","morana.prole.org":"10.0.0.66","morgoth.prole.org":"10.0.0.204","myrddin.prole.org":"10.0.0.3","pi.prole.org":"10.0.0.5","raspberry.prole.org":"10.0.0.4","retropie.prole.org":"10.0.0.207","synology.prole.org":"10.0.0.203","zinfandel.prole.org":"10.0.0.205"},"ad_dc":{"host":"myrddin.prole.org","ip":"10.0.0.3"},"k3s":{"server_url":"https://myrddin.prole.org:6443","server_host":"myrddin.prole.org","token_present":true},"groups":{"iscsi":["pi.prole.org","raspberry.prole.org","myrddin.prole.org","retropie.prole.org","merlin.prole.org"],"pihole":["pi.prole.org","raspberry.prole.org"],"ad_dc":["myrddin.prole.org"],"k3s_servers":["myrddin.prole.org"],"k3s_agents":["pi.prole.org","merlin.prole.org"],"k3s_hosts:children":["k3s_servers","k3s_agents"],"linux_hosts":["pi.prole.org","raspberry.prole.org","myrddin.prole.org","retropie.prole.org","merlin.prole.org"],"mariadb_primary":["merlin.prole.org"],"mariadb_replica":["raspberry.prole.org"],"mariadb:children":["mariadb_primary","mariadb_replica"],"merlin_bootstrap":["merlin"]},"hosts":{"merlin":"10.0.0.6","merlin.prole.org":"10.0.0.6","myrddin.prole.org":"10.0.0.3","pi.prole.org":"10.0.0.5","raspberry.prole.org":"10.0.0.4","retropie.prole.org":"10.0.0.207"},"unmapped_hosts":["k3s_agents","k3s_servers","mariadb_primary","mariadb_replica"]} -KDC_ANSIBLE_DETECTED = 10.0.0.3 -KDC_AUTO_DETECTED = 10.0.0.3 -KERBEROS_AUTO_ENABLED = True - -[Port Forwards] -; No configuration values captured yet for this section. - -[System Environment] -PROLE_CONF = ${PROLE_CONF} -PROLE_DATA = ${PROLE_DATA} -PROLE_HOME = ${PROLE_HOME} -PROLE_LOGS = ${PROLE_LOGS} -PROLE_SERVICE = ${PROLE_SERVICE} - -[Monitoring] -; No configuration values captured yet for this section. - -[Kerberos Authentication] -; No configuration values captured yet for this section. - -[Ollama] -; No configuration values captured yet for this section. - -[Optional Features] -AT_REST_ENCRYPTION_ENABLED = True -GITOPS_ENABLED = False -KERBEROS_ENABLED = True -SUPABASE_ENABLED = False - -[GitOps] -; No configuration values captured yet for this section. - -[Database Creation] -NAMESPACE = ${NAMESPACE} - -[Initialize Cluster] -ENVIRONMENT = service -K3S_SERVER_URL = https://myrddin.prole.org:6443 -K3S_TOKEN = ${PROLE_SECRET:v1:i1FXxxDV0iTGUprA:Esd5SwMPbXNiWB1FeAPJqDVMhaIwg6TeR7aPH-IbF3HURxH6qs89IPQWhAAOmuFYLNCG_Fc8e7y6kTbIrV-md-fOVOZ8PbRHwGp_6u2w7O5WUf_QIdhxF30H-UQZlFZHOAHnW2l970O2zQQPGRJkqQq6VzoK2_P6tiJ8uA==} - -[Dev Cluster (k3d)] -CLUSTER_ENV = dev -DISPLAY_NAME = knoe-dev-cluster -KUBECTL_CONTEXT = prole-knoe-db3s -MODE = k3d - -[Service Cluster (k3s)] -CLUSTER_ENV = prole-service-cluster -DISPLAY_NAME = prole-service-cluster -K3S_SERVER_URL = https://myrddin.prole.org:6443 -K3S_TOKEN = ${PROLE_SECRET:v1:i1FXxxDV0iTGUprA:Esd5SwMPbXNiWB1FeAPJqDVMhaIwg6TeR7aPH-IbF3HURxH6qs89IPQWhAAOmuFYLNCG_Fc8e7y6knoe-dbTbIrV-md-fOVOZ8PbRHwGp_6u2w7O5WUf_QIdhxF30H-UQZlFZHOAHnW2l970O2zQQPGRJknoe-dbqQq6VzoK2_P6tiJ8uA==} -MODE = k3s -PIPELINE_URL = http://127.0.0.1:8080 - -[Prod Cluster (k8s)] -ARTIFACTS_DIR = /Users/chrisfu/dev/prole/data/staging -CLUSTER_ENV = prole-prod-cluster -DISPLAY_NAME = prole-prod-cluster -MODE = k8s -PIPELINE_URL = http://127.0.0.1:8080 - -[Docker Build] -LOCAL_REGISTRY = localhost:5000 -LOCAL_REGISTRY_INTERNAL = k3d-prole-registry.localhost:5000 - -[Initialization Scripts] -; No configuration values captured yet for this section. - -[Deployment] -MODE = k3s -TARGET = prole-service-cluster - -[Install] -; No configuration values captured yet for this section. diff --git a/installer/config.py b/installer/config.py index 5e104b8..9a0a46b 100644 --- a/installer/config.py +++ b/installer/config.py @@ -13,6 +13,7 @@ import json import os import platform import re +import shlex import shutil import subprocess import sys @@ -749,17 +750,79 @@ DEPENDENCIES = [ ] +def _resolve_brew_bin() -> str | None: + """Return an absolute path to a `brew` binary if one is found in common locations.""" + + candidates: list[str] = [ + "/home/linuxbrew/.linuxbrew/bin/brew", # Linuxbrew (recommended) + os.path.expanduser("~/.linuxbrew/bin/brew"), # Linuxbrew (legacy single-user) + "/opt/homebrew/bin/brew", # macOS (Apple Silicon) + "/usr/local/bin/brew", # macOS (Intel) + ] + + for p in candidates: + try: + if os.path.isfile(p) and os.access(p, os.X_OK): + return p + except Exception: + continue + return None + + +def _augment_env_for_brew(env: dict | None = None) -> dict: + """Prepend Homebrew `bin`/`sbin` to PATH when brew exists but isn't in PATH. + + This keeps dependency checks and `brew install ...` invocations working in + non-interactive environments where shell init files aren't sourced. + """ + + if env is None: + env = os.environ.copy() + + brew_bin = _resolve_brew_bin() + if not brew_bin: + return env + + try: + brew_bin_path = Path(brew_bin) + prefix = brew_bin_path.parent.parent + bin_dir = str(prefix / "bin") + sbin_dir = str(prefix / "sbin") + + path_val = env.get("PATH", "") or "" + parts = [p for p in path_val.split(os.pathsep) if p] + + prepend: list[str] = [] + if bin_dir and bin_dir not in parts: + prepend.append(bin_dir) + try: + if os.path.isdir(sbin_dir) and sbin_dir not in parts: + prepend.append(sbin_dir) + except Exception: + # sbin is optional + pass + + if prepend: + env["PATH"] = os.pathsep.join(prepend + parts) + except Exception: + return env + + return env + + 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()) if bin_name: res = subprocess.run( - ["bash", "-lc", f"command -v {bin_name}"], + ["bash", "-lc", f"command -v -- {shlex.quote(bin_name)}"], capture_output=True, text=True, + env=env, ) if res.returncode == 0: location = res.stdout.strip() @@ -768,7 +831,10 @@ def get_dep_info(dep: dict) -> Tuple[bool, Optional[str], Optional[str]]: check_cmd = dep.get("check_cmd") if check_cmd: res2 = subprocess.run( - ["bash", "-lc", check_cmd], capture_output=True, text=True + ["bash", "-lc", check_cmd], + capture_output=True, + text=True, + env=env, ) if res2.returncode == 0: installed = True @@ -781,7 +847,10 @@ def get_dep_info(dep: dict) -> Tuple[bool, Optional[str], Optional[str]]: version_cmd = dep.get("version_cmd") if version_cmd: res3 = subprocess.run( - ["bash", "-lc", version_cmd], capture_output=True, text=True + ["bash", "-lc", version_cmd], + capture_output=True, + text=True, + env=env, ) if res3.returncode == 0: v_out = res3.stdout.strip() diff --git a/installer/milestone.py b/installer/milestone.py index 79c1265..ee93a16 100644 --- a/installer/milestone.py +++ b/installer/milestone.py @@ -53,9 +53,11 @@ class Milestone(ABC): if isinstance(cmd, str): cmd = ["bash", "-c", cmd] + import os import subprocess try: + env = inst_config._augment_env_for_brew(os.environ.copy()) proc = subprocess.Popen( cmd, stdout=subprocess.PIPE, @@ -63,6 +65,7 @@ class Milestone(ABC): text=True, bufsize=1, cwd=cwd, + env=env, ) for line in iter(proc.stdout.readline, ""): if on_stdout: diff --git a/tests/installer/test_config_helpers.py b/tests/installer/test_config_helpers.py index a8cb83b..61867a4 100644 --- a/tests/installer/test_config_helpers.py +++ b/tests/installer/test_config_helpers.py @@ -270,6 +270,42 @@ def test_get_dep_info_check_cmd_success(): assert "Docker" in (ver or "") +def test_get_dep_info_prepends_linuxbrew_to_path_when_present(): + dep = { + "id": "brew", + "name": "Homebrew", + "bin": "brew", + "check_cmd": "brew --version", + } + + def _run(args, capture_output=False, text=False, env=None, **kwargs): + assert env is not None + path_parts = (env.get("PATH") or "").split(os.pathsep) + assert path_parts[0] == "/home/linuxbrew/.linuxbrew/bin" + assert "/home/linuxbrew/.linuxbrew/sbin" in path_parts[:2] + + m = MagicMock() + cmd = " ".join(args) + if "command -v" in cmd: + m.returncode = 0 + m.stdout = "/home/linuxbrew/.linuxbrew/bin/brew\n" + else: + m.returncode = 0 + m.stdout = "Homebrew 5.0.0\n" + return m + + with patch.dict(os.environ, {"PATH": "/usr/bin:/bin"}, clear=False), \ + patch("installer.config.os.path.isfile", side_effect=lambda p: p == "/home/linuxbrew/.linuxbrew/bin/brew"), \ + patch("installer.config.os.access", return_value=True), \ + patch("installer.config.os.path.isdir", side_effect=lambda p: p == "/home/linuxbrew/.linuxbrew/sbin"), \ + patch("installer.config.subprocess.run", side_effect=_run): + ok, loc, ver = get_dep_info(dep) + + assert ok is True + assert loc == "/home/linuxbrew/.linuxbrew/bin/brew" + assert ver == "Homebrew 5.0.0" + + def test_get_dep_info_version_cmd_success(): dep = {"id": "kubectl", "name": "kubectl", "bin": "kubectl", "version_cmd": "kubectl version --client --short"}