#!/bin/bash set -euo pipefail ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" # Logging setup LOG_DIR="${PROLE_LOGS:-$ROOT_DIR/logs}" LOG_DIR="${LOG_DIR%/}" mkdir -p "$LOG_DIR" TIMESTAMP="$(date +%Y%m%d-%H%M%S)" LOG_FILE="${LOG_DIR}/silent_install_${TIMESTAMP}.log" # Function to handle output redirection if [[ "${SILENT_INSTALL_LOG:-}" == "true" ]]; then echo "Logging output to: $LOG_FILE" # Redirect stdout and stderr to both terminal and log file exec > >(tee -a "$LOG_FILE") 2>&1 fi CFG_PATH="${1:-}" if [[ -z "$CFG_PATH" ]]; then if [[ -n "${PROLE_TEST_CFG:-}" ]]; then if [[ "$PROLE_TEST_CFG" == "true" || "$PROLE_TEST_CFG" == "1" ]]; then CFG_PATH="$ROOT_DIR/tests/fixtures/test-prole.cfg" else CFG_PATH="$PROLE_TEST_CFG" fi elif [[ -n "${PROLE_CONF:-}" && -f "$PROLE_CONF/prole.cfg" ]]; then CFG_PATH="$PROLE_CONF/prole.cfg" elif [[ -f "$ROOT_DIR/conf/prole.cfg" ]]; then CFG_PATH="$ROOT_DIR/conf/prole.cfg" else echo "ERROR: prole.cfg not found. Pass a path or set PROLE_CONF." >&2 exit 1 fi fi CFG_DIR="$(cd "$(dirname "$CFG_PATH")" && pwd)" if [[ -z "${PROLE_CONF:-}" ]]; then PROLE_CONF="$CFG_DIR" export PROLE_CONF fi TMP_CFG="" if [[ "${PROLE_CFG_INPLACE:-}" != "true" ]]; then TMP_CFG="$(mktemp -t prole.cfg.XXXXXX)" cp "$CFG_PATH" "$TMP_CFG" CFG_PATH="$TMP_CFG" trap '[[ -n "$TMP_CFG" ]] && rm -f "$TMP_CFG"' EXIT fi echo "==============================================" echo " SILENT INSTALL TEST" echo " prole.cfg: $CFG_PATH" echo "==============================================" echo "" echo "1. Validating prole.cfg..." python3 - "$CFG_PATH" <<'PY' import configparser import os import sys from pathlib import Path cfg_path = Path(sys.argv[1]).expanduser() if not cfg_path.exists(): print(f"ERROR: prole.cfg not found at {cfg_path}", file=sys.stderr) sys.exit(1) cfg = configparser.ConfigParser(interpolation=None) cfg.optionxform = str cfg.read(cfg_path) required_sections = ["Inputs", "Global"] missing_sections = [s for s in required_sections if not cfg.has_section(s)] if missing_sections: print(f"ERROR: Missing sections: {', '.join(missing_sections)}", file=sys.stderr) sys.exit(1) required_inputs = [ "build.deploy_env", "build.run_build", "dependencies.auto_install_missing", "dependencies.brew.install", "dependencies.docker.install", "dependencies.k3d.install", "dependencies.verify_all", "disk_selection.disk_type", "disk_selection.removable_mount", "disk_selection.local_path", "env_setup.NAMESPACE", "env_setup.PROLE_CONF", "env_setup.PROLE_DATA", "env_setup.PROLE_HOME", "env_setup.PROLE_LOGS", "env_setup.PROLE_SERVICE", "init_cluster.at_rest_encryption_enabled", "init_cluster.cluster_env", "init_cluster.kerberos_enabled", "init_cluster.start_cluster", "init_cluster.supabase_enabled", "init_cnpg_deploy.force_rollout", "init_cnpg_deploy.run_deploy", "init_db_build.run_build", "init_password.db_host_port", "init_password.db_namespace", "init_password.db_password", "init_password.db_password_confirm", "init_password.db_username", "init_password.generate_ssh_key", "init_scripts.run_scripts", "kerberos_config.enabled", "kerberos_config.init_authority", "kerberos_config.kdc", "kerberos_config.password", "kerberos_config.realm", "kerberos_config.test_connection", "kerberos_config.user", "network_scan.run", ] missing_inputs = [k for k in required_inputs if not cfg.has_option("Inputs", k)] if missing_inputs: print("ERROR: Missing Inputs keys:", file=sys.stderr) for key in missing_inputs: print(f" - {key}", file=sys.stderr) sys.exit(1) required_non_empty = { "Inputs": [ "env_setup.PROLE_HOME", "env_setup.PROLE_CONF", "env_setup.PROLE_DATA", "env_setup.PROLE_LOGS", "env_setup.PROLE_SERVICE", "env_setup.NAMESPACE", "init_password.db_namespace", "init_password.db_username", "init_password.db_host_port", ], "Global": [ "PROLE_HOME", "KNOE_DB_USER", "CLUSTER_ENV", "NAMESPACE", ], } def is_anchor(val: str) -> bool: val = val.strip() return val.startswith("${OPENBAO:") or val.startswith("${PROLE_SECRET:") errors = [] warnings = [] for section, keys in required_non_empty.items(): for key in keys: if not cfg.has_option(section, key): errors.append(f"{section}.{key} missing") continue value = cfg.get(section, key, fallback="").strip() if not value: errors.append(f"{section}.{key} is empty") secret_keys = { ("Inputs", "init_password.db_password"), ("Inputs", "init_password.db_password_confirm"), ("Global", "DB_PASSWORD"), } for section, key in secret_keys: if not cfg.has_option(section, key): errors.append(f"{section}.{key} missing") continue value = cfg.get(section, key, fallback="").strip() if not value: errors.append(f"{section}.{key} is empty") elif is_anchor(value): warnings.append(f"{section}.{key} is anchored ({value})") pw = cfg.get("Inputs", "init_password.db_password", fallback="") pw_confirm = cfg.get("Inputs", "init_password.db_password_confirm", fallback="") if pw and pw_confirm and pw != pw_confirm: errors.append("Inputs.init_password.db_password does not match init_password.db_password_confirm") if errors: print("ERROR: prole.cfg validation failed:", file=sys.stderr) for err in errors: print(f" - {err}", file=sys.stderr) sys.exit(1) if warnings: print("WARN: prole.cfg contains anchored secrets:") for warn in warnings: print(f" - {warn}") print("OK: prole.cfg validation") PY echo "" echo "2. Running silent install..." if "$ROOT_DIR/install.sh" -S -c "$CFG_PATH"; then echo "OK: Silent install completed" else echo "ERROR: Silent install failed" >&2 exit 1 fi echo "" echo "3. Blessing sanitized gold config..." python3 - "$CFG_PATH" "$PROLE_CONF/knoe-db/prole.cfg" <<'PY' import re import sys from pathlib import Path import configparser source = Path(sys.argv[1]).expanduser() target = Path(sys.argv[2]).expanduser() target.parent.mkdir(parents=True, exist_ok=True) text = source.read_text() cfg = configparser.ConfigParser(interpolation=None) cfg.optionxform = str cfg.read(source) var_pattern = re.compile(r"\$(\w+)|\$\{(\w+)\}") def expand_cfg_value(val: str, cfg_vars: dict) -> str: if val is None: return "" raw = str(val) def repl(match): var = match.group(1) or match.group(2) return str(cfg_vars.get(var, match.group(0))) out = raw for _ in range(5): new = var_pattern.sub(repl, out) if new == out: break out = new return out cfg_vars = {} for section in cfg.sections(): for k, v in cfg.items(section): if k in cfg_vars: continue cfg_vars[k] = expand_cfg_value(v, cfg_vars) namespace = ( expand_cfg_value(cfg.get("Global", "NAMESPACE", fallback=""), cfg_vars) or expand_cfg_value(cfg.get("Inputs", "env_setup.NAMESPACE", fallback=""), cfg_vars) or "default" ).strip() or "default" def bao(leaf: str, key: str) -> str: return f"${{OPENBAO:kv/prole/{namespace}/{leaf}#{key}}}" secret_map = { "init_password.db_password": bao("db", "password"), "init_password.db_password_confirm": bao("db", "password"), "kerberos_config.password": bao("kerberos", "password"), "DB_PASSWORD": bao("db", "password"), "PASSWORD": bao("kerberos", "password"), "GRAFANA_ADMIN_PASSWORD": bao("monitoring", "grafana_admin_password"), } pattern = re.compile(r"^(?P\s*)(?P[^=]+?)(?P
\s*)=(?P\s*).*$")
out_lines = []

for line in text.splitlines():
    stripped = line.strip()
    if not stripped or stripped.startswith((';', '#')) or '=' not in line:
        out_lines.append(line)
        continue
    m = pattern.match(line)
    if not m:
        out_lines.append(line)
        continue
    key = m.group("key").strip()
    lead = m.group("lead")
    pre = m.group("pre")
    post = m.group("post")
    if key in secret_map:
        value = secret_map[key]
        out_lines.append(f"{lead}{key}{pre}={post}{value}")
        continue
    if re.search(r"(?i)(?:^|[_.])(password|token|secret)$", key):
        out_lines.append(f"{lead}{key}{pre}={post}${{OPENBAO:REDACTED}}")
        continue
    out_lines.append(line)

target.write_text("\n".join(out_lines) + "\n")
print(f"OK: Wrote sanitized gold config: {target}")
PY

echo ""
echo "=============================================="
echo "  OK: Silent Install Test Completed"
echo "=============================================="
echo ""