diff --git a/conf/prole.cfg b/conf/prole.cfg index da80b8c..151ef40 100644 --- a/conf/prole.cfg +++ b/conf/prole.cfg @@ -6,9 +6,9 @@ ; 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_DATA = ${HOME}/.prole/data +PROLE_HOME = ${HOME}/dev/prole +PROLE_LOGS = /opt/prole/logs/${USER} PROLE_SERVICE = ${PROLE_HOME}/etc SERVICE_NAMESPACE = knoe-system @@ -166,7 +166,7 @@ MODE = k3s PIPELINE_URL = http://127.0.0.1:8080 [Prod Cluster (k8s)] -ARTIFACTS_DIR = /Users/chrisfu/dev/prole/data/staging +ARTIFACTS_DIR = ${PROLE_HOME}/data/staging CLUSTER_ENV = prole-prod-cluster DISPLAY_NAME = prole-prod-cluster MODE = k8s diff --git a/deploy/opentofu/k3s/opentofu.auto.tfvars b/deploy/opentofu/k3s/opentofu.auto.tfvars index cb23e96..2d287ab 100644 --- a/deploy/opentofu/k3s/opentofu.auto.tfvars +++ b/deploy/opentofu/k3s/opentofu.auto.tfvars @@ -1,4 +1,4 @@ k3s_server_url = "https://myrddin.prole.org:6443" k3s_token = "K107c8c6000488eca4a067d8a73119bbae2f07b4ea1bac7d8d3dc9c500cbb8acb18::server:04572345810eae2f9619a6ed4239702b" namespace = "prole-db" -kubeconfig_path = "/Users/chrisfu/dev/prole/prole-k3s.kubeconfig" +kubeconfig_path = "../../../prole-k3s.kubeconfig" diff --git a/env.sh b/env.sh index 728ab40..f15680c 100755 --- a/env.sh +++ b/env.sh @@ -3,11 +3,12 @@ # This file is generated by the installer. Source it in new shells, or execute as a wrapper: # "$PROLE_HOME/env.sh" [args…] # shellcheck shell=bash -export PROLE_HOME="/Users/chrisfu/dev/prole" -export PROLE_CONF="/Users/chrisfu/dev/prole/conf" -export PROLE_DATA="/Users/chrisfu/.prole/data" -export PROLE_LOGS="/opt/prole/logs/chrisfu" -export PROLE_SERVICE="/Users/chrisfu/dev/prole/etc" +PROLE_HOME="${PROLE_HOME:-$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)}" +export PROLE_HOME +export PROLE_CONF="${PROLE_CONF:-$PROLE_HOME/conf}" +export PROLE_DATA="${PROLE_DATA:-$HOME/.prole/data}" +export PROLE_LOGS="${PROLE_LOGS:-/opt/prole/logs/${USER}}" +export PROLE_SERVICE="${PROLE_SERVICE:-$PROLE_HOME/etc}" # Ensure PATH works for GUI-launched shells (Docker, etc.) _prole_add_path() { case ":${PATH}:" in *":$1:"*) ;; *) PATH="$1:${PATH:-}" ;; esac; } diff --git a/etc/init_port_forwards.sh b/etc/init_port_forwards.sh index a6391a6..78353c9 100755 --- a/etc/init_port_forwards.sh +++ b/etc/init_port_forwards.sh @@ -11,7 +11,7 @@ SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) # shellcheck disable=SC1090 source "$SCRIPT_DIR/prole_cfg.sh" -PROLE_HOME="${PROLE_HOME:-/Users/chrisfu/dev/prole}" +PROLE_HOME="${PROLE_HOME:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)}" VERBOSE=0 PROLE_CFG_OVERRIDE="" PORT_MAPPING_FILE_PATH="" diff --git a/installer/core/actions.py b/installer/core/actions.py index c97eabc..3554e35 100644 --- a/installer/core/actions.py +++ b/installer/core/actions.py @@ -462,6 +462,17 @@ class ProleInstallerBase: # --------------------------------------------- cfg sanitisation def _sanitize_sections_for_cfg(self, sections: dict) -> dict: sanitized = {k: dict(v) for k, v in sections.items()} + + # Avoid persisting machine-specific home paths (e.g. /Users//...) in prole.cfg. + # Use ${HOME} where possible, and also rewrite stale foreign-home paths that match + # the current username (common when HOME is shared across machines). + for section_name, section in sanitized.items(): + for k, v in list(section.items()): + if not isinstance(v, str) or not v.strip(): + continue + if _is_prole_secret(v) or _is_openbao_ref(v): + continue + section[k] = self._cfgify_home_path(v) if "Kerberos Authentication" in sanitized: val = sanitized["Kerberos Authentication"].get("PASSWORD", "") if val or ("Kerberos Authentication", "PASSWORD") in self._cfg_secret_cache: @@ -590,6 +601,92 @@ class ProleInstallerBase: return "".join(secrets.choice(alphabet) for _ in range(length)) # --------------------------------------------------- env helpers + + _FOREIGN_HOME_PREFIX_RE = re.compile(r"^/(Users|home)/([^/]+)(/.*)?$") + + def _rewrite_foreign_home_prefix(self, value: str) -> str: + """Rewrite foreign home prefixes to the current user's home. + + Example: running on Linux with HOME=/home/, rewrite + `/Users//dev/prole` -> `/home//dev/prole`. + + This helps prevent stale macOS paths (often stored in `~/.prole/env.sh`) from + being persisted into `prole.cfg` on Linux. + """ + + raw = (value or "").strip() + if not raw or "$" in raw: + return raw + + m = self._FOREIGN_HOME_PREFIX_RE.match(raw) + if not m: + return raw + + foreign_root = m.group(1) + foreign_user = m.group(2) + suffix = m.group(3) or "" + + try: + current_home = Path.home() + except Exception: + return raw + + # Only rewrite when the path refers to the current username. + if foreign_user != current_home.name: + return raw + + system = platform.system() + if system != "Darwin" and foreign_root == "Users": + return str(current_home) + suffix + if system == "Darwin" and foreign_root == "home": + return str(current_home) + suffix + + return raw + + def _shellify_home_path(self, value: str) -> str: + """Represent paths under the current home directory using `$HOME`. + + Keeps values unchanged if they already contain shell variables. + """ + + raw = (value or "").strip() + if not raw or "$" in raw: + return raw + try: + p = Path(raw).expanduser() + home = Path.home() + rel = p.relative_to(home) + except Exception: + return raw + rel_str = rel.as_posix() + if not rel_str or rel_str == ".": + return "$HOME" + return f"$HOME/{rel_str}" + + def _cfgify_home_path(self, value: str) -> str: + """Represent paths under the current home directory using `${HOME}`. + + Also rewrites stale foreign-home paths like `/Users//...` when the + username matches the current user. + """ + + raw = (value or "").strip() + if not raw or "$" in raw: + return raw + + raw = self._rewrite_foreign_home_prefix(raw) + + try: + p = Path(raw).expanduser() + home = Path.home() + rel = p.relative_to(home) + except Exception: + return raw + rel_str = rel.as_posix() + if not rel_str or rel_str == ".": + return "${HOME}" + return f"${{HOME}}/{rel_str}" + def _env_defaults(self, namespace: str | None = None) -> dict: default_home = Path.home() / ".prole" resolved_home = self._resolve_env_value("PROLE_HOME", str(default_home)) or str( @@ -662,7 +759,17 @@ class ProleInstallerBase: line = line[len("export ") :] if "=" in line: k, v = line.split("=", 1) - env[k.strip()] = v.strip().strip('"') + k = k.strip() + v = v.strip().strip('"') + if k in { + "PROLE_HOME", + "PROLE_CONF", + "PROLE_DATA", + "PROLE_LOGS", + "PROLE_SERVICE", + }: + v = self._rewrite_foreign_home_prefix(v) + env[k] = v break except Exception: pass @@ -696,7 +803,7 @@ class ProleInstallerBase: "PROLE_LOGS", "PROLE_SERVICE", ): - content.append(f'export {k}="{values[k]}"') + content.append(f'export {k}="{self._shellify_home_path(values[k])}"') # NAMESPACE/PROLE_NAMESPACE must never be written to env.sh — only prole.cfg is the source of truth content.append("") content.append("# Ensure PATH works for GUI-launched shells (Docker, etc.)") diff --git a/mock_val/init_port_forwards.sh b/mock_val/init_port_forwards.sh index a6391a6..78353c9 100755 --- a/mock_val/init_port_forwards.sh +++ b/mock_val/init_port_forwards.sh @@ -11,7 +11,7 @@ SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) # shellcheck disable=SC1090 source "$SCRIPT_DIR/prole_cfg.sh" -PROLE_HOME="${PROLE_HOME:-/Users/chrisfu/dev/prole}" +PROLE_HOME="${PROLE_HOME:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)}" VERBOSE=0 PROLE_CFG_OVERRIDE="" PORT_MAPPING_FILE_PATH="" diff --git a/prole-mssql-db/docker-root-mssql-server.sh b/prole-mssql-db/docker-root-mssql-server.sh index e86c52a..c5c743d 100755 --- a/prole-mssql-db/docker-root-mssql-server.sh +++ b/prole-mssql-db/docker-root-mssql-server.sh @@ -4,11 +4,13 @@ if [[ -z "${MSSQL_SA_PASSWORD:-}" ]]; then exit 1 fi +PROLE_HOME="${PROLE_HOME:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)}" + DOCKER_DEFAULT_PLATFORM=linux/amd64 docker run --rm -it \ --hostname=prole-mssql-db001 -p 1433:1433 \ -e "ACCEPT_EULA=Y" -e "MSSQL_SA_PASSWORD=$MSSQL_SA_PASSWORD" \ --env=PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin \ - --volume=/Users/chrisfu/dev/prole/mssql/data:/var/opt/mssql/data \ + --volume="${PROLE_HOME}/mssql/data:/var/opt/mssql/data" \ --entrypoint bash \ --restart=no \ mcr.microsoft.com/mssql/server:2022-latest diff --git a/prole-mssql-db/docker-root-prole-mssql-db.sh b/prole-mssql-db/docker-root-prole-mssql-db.sh index 1fb4c93..37333da 100755 --- a/prole-mssql-db/docker-root-prole-mssql-db.sh +++ b/prole-mssql-db/docker-root-prole-mssql-db.sh @@ -4,11 +4,13 @@ if [[ -z "${MSSQL_SA_PASSWORD:-}" ]]; then exit 1 fi +PROLE_HOME="${PROLE_HOME:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)}" + DOCKER_DEFAULT_PLATFORM=linux/amd64 docker run --rm -it \ --hostname=prole-mssql-db001 -p 1433:1433 \ -e "ACCEPT_EULA=Y" -e "MSSQL_SA_PASSWORD=$MSSQL_SA_PASSWORD" \ --env=PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin \ - --volume=/Users/chrisfu/dev/prole/mssql/data:/var/opt/mssql/data \ + --volume="${PROLE_HOME}/mssql/data:/var/opt/mssql/data" \ --entrypoint bash \ --restart=no \ prole-mssql-db:016 diff --git a/prole-mssql-db/docker-run-mssql-server.sh b/prole-mssql-db/docker-run-mssql-server.sh index 4d6f840..0e06f0a 100755 --- a/prole-mssql-db/docker-run-mssql-server.sh +++ b/prole-mssql-db/docker-run-mssql-server.sh @@ -6,7 +6,9 @@ if [[ -z "${MSSQL_SA_PASSWORD:-}" ]]; then exit 1 fi +PROLE_HOME="${PROLE_HOME:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)}" + docker run -e "ACCEPT_EULA=Y" -e "MSSQL_SA_PASSWORD=$MSSQL_SA_PASSWORD" \ -p 1433:1433 --name mssql-server --platform linux/amd64 \ - -v /Users/chrisfu/dev/prole/mssql/data:/var/opt/mssql/data \ + -v "${PROLE_HOME}/mssql/data:/var/opt/mssql/data" \ -d mcr.microsoft.com/mssql/server:2022-latest diff --git a/prole-mssql-db/docker-run-prole-mssql-db.sh b/prole-mssql-db/docker-run-prole-mssql-db.sh index 0e2dff8..e9488c0 100755 --- a/prole-mssql-db/docker-run-prole-mssql-db.sh +++ b/prole-mssql-db/docker-run-prole-mssql-db.sh @@ -4,11 +4,13 @@ if [[ -z "${MSSQL_SA_PASSWORD:-}" ]]; then exit 1 fi +PROLE_HOME="${PROLE_HOME:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)}" + DOCKER_DEFAULT_PLATFORM=linux/amd64 docker run --rm -it \ --hostname=prole-mssql-db-1 \ -p 1433:1433 \ -e "ACCEPT_EULA=Y" \ -e "MSSQL_SA_PASSWORD=$MSSQL_SA_PASSWORD" \ -e "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" \ - -v /Users/chrisfu/dev/prole/mssql/data:/var/opt/mssql/data \ + -v "${PROLE_HOME}/mssql/data:/var/opt/mssql/data" \ prole-mssql-db:015 diff --git a/tests/installer/conftest.py b/tests/installer/conftest.py index f6ad340..dc6f990 100644 --- a/tests/installer/conftest.py +++ b/tests/installer/conftest.py @@ -6,9 +6,16 @@ test_screen.py don't hit exhausted side-effect iterators left over from test_navigation.py (which replaces sys.modules["tkinter"] at module level). """ import sys +from pathlib import Path import pytest from unittest.mock import MagicMock +# Ensure the repository root is first on sys.path so we import the local +# `installer` package (and not a site-packages name collision). +_REPO_ROOT = Path(__file__).resolve().parents[2] +if str(_REPO_ROOT) not in sys.path: + sys.path.insert(0, str(_REPO_ROOT)) + import installer.screen as _screen_module diff --git a/tests/installer/test_actions_helpers.py b/tests/installer/test_actions_helpers.py index 18ce2c3..0a68656 100644 --- a/tests/installer/test_actions_helpers.py +++ b/tests/installer/test_actions_helpers.py @@ -332,6 +332,68 @@ class TestNamespaceHelpers: assert inst._registry_namespace() == "knoe-system" +# --------------------------------------------------------------------------- +# env.sh portability (avoid hardcoded /Users/ paths) +# --------------------------------------------------------------------------- + + +class TestEnvPortability: + def test_env_defaults_rewrites_foreign_macos_home_paths_on_linux( + self, tmp_path, monkeypatch + ): + # Simulate a shared/synced HOME that contains an env.sh written on macOS. + fake_home = tmp_path / "testuser" + (fake_home / ".prole").mkdir(parents=True) + (fake_home / ".prole" / "env.sh").write_text( + '\n'.join( + [ + 'export PROLE_HOME="/Users/testuser/dev/prole"', + 'export PROLE_CONF="/Users/testuser/dev/prole/conf"', + 'export PROLE_DATA="/Users/testuser/.prole/data"', + "", + ] + ) + ) + + inst = _TestableInstaller(project_root=tmp_path) + + monkeypatch.setattr("installer.core.actions.platform.system", lambda: "Linux") + monkeypatch.setattr("installer.core.actions.Path.home", lambda: fake_home) + + with mock.patch.dict(os.environ, {}, clear=False): + os.environ.pop("PROLE_HOME", None) + os.environ.pop("PROLE_CONF", None) + os.environ.pop("PROLE_DATA", None) + env = inst._env_defaults(namespace="prole-db") + + assert env["PROLE_HOME"] == str(fake_home / "dev" / "prole") + assert env["PROLE_CONF"] == str(fake_home / "dev" / "prole" / "conf") + assert env["PROLE_DATA"] == str(fake_home / ".prole" / "data") + + def test_save_env_to_file_uses_home_var_not_absolute_home(self, tmp_path, monkeypatch): + fake_home = tmp_path / "testuser" + prole_home = fake_home / "dev" / "prole" + (prole_home / "conf").mkdir(parents=True) + (prole_home / "etc").mkdir(parents=True) + + inst = _TestableInstaller(project_root=tmp_path) + monkeypatch.setattr("installer.core.actions.Path.home", lambda: fake_home) + + values = { + "PROLE_HOME": str(prole_home), + "PROLE_CONF": str(prole_home / "conf"), + "PROLE_DATA": str(fake_home / ".prole" / "data"), + "PROLE_LOGS": "/opt/prole/logs/chrisfu", + "PROLE_SERVICE": str(prole_home / "etc"), + } + + inst._save_env_to_file(values) + + env_sh = (prole_home / "env.sh").read_text() + assert 'export PROLE_HOME="$HOME/dev/prole"' in env_sh + assert str(fake_home) not in env_sh + + # --------------------------------------------------------------------------- # _read_k3s_cfg_values # --------------------------------------------------------------------------- diff --git a/tests/test_network_scan_fix.sh b/tests/test_network_scan_fix.sh index 9f21166..03b9480 100755 --- a/tests/test_network_scan_fix.sh +++ b/tests/test_network_scan_fix.sh @@ -47,9 +47,10 @@ fi # Test 4: prole-agent can run from scan directory echo "4. Testing prole-agent runs from writable directory..." +PROLE_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" mkdir -p /tmp/scan-test cd /tmp/scan-test -/Users/chrisfu/dev/prole/prole-net/prole-agent 2>&1 & +"$PROLE_ROOT/prole-net/prole-agent" 2>&1 & SCAN_PID=$! sleep 2 if ps -p $SCAN_PID > /dev/null 2>&1; then @@ -60,7 +61,7 @@ else echo " ✗ prole-agent failed to start" exit 1 fi -cd /Users/chrisfu/dev/prole +cd "$PROLE_ROOT" # Test 5: Code uses scan directory echo "5. Testing code uses scan directory..."