Fix installer path portability (avoid hardcoded /Users paths)

- Rewrite stale foreign home prefixes when loading env defaults (macOS <-> Linux)

- Write /Users/chrisfu-relative values to env.sh; sanitize prole.cfg paths to use ${HOME}

- Remove remaining /Users/chrisfu defaults from helper scripts and OpenTofu kubeconfig path

- Make installer tests more reliable by forcing repo root to front of sys.path

Co-authored-by: Junie <junie@jetbrains.com>
This commit is contained in:
chrisfu 2026-03-09 16:55:49 -07:00
parent 2dc8726395
commit caa2685fc2
13 changed files with 206 additions and 20 deletions

View File

@ -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

View File

@ -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"

11
env.sh
View File

@ -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" <command> [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; }

View File

@ -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=""

View File

@ -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/<user>/...) 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/<user>, rewrite
`/Users/<user>/dev/prole` -> `/home/<user>/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/<user>/...` 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.)")

View File

@ -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=""

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -332,6 +332,68 @@ class TestNamespaceHelpers:
assert inst._registry_namespace() == "knoe-system"
# ---------------------------------------------------------------------------
# env.sh portability (avoid hardcoded /Users/<user> 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
# ---------------------------------------------------------------------------

View File

@ -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..."