feat: strip config.py to bootstrap only (db_url + auto-gen secret_key)

This commit is contained in:
2026-03-18 20:32:02 -04:00
parent 80ce703a15
commit 03bbd8d0fa
+60 -42
View File
@@ -1,63 +1,81 @@
# fablednetmon/config.py
from __future__ import annotations from __future__ import annotations
import logging
import os import os
import secrets
from pathlib import Path from pathlib import Path
from typing import Any from typing import Any
import yaml
from dotenv import load_dotenv
REQUIRED_KEYS = ["secret_key", "database.url"] logger = logging.getLogger(__name__)
_PREFIX = "FABLEDNETMON_"
_SECRET_KEY_FILE = Path("/data/secret.key")
def load_config(config_path: Path | str | None = None) -> dict[str, Any]: def load_bootstrap(config_path: Path | str | None = None) -> dict[str, str]:
"""Load config from YAML file with env var overrides. """Return the minimum bootstrap config: database_url and secret_key.
Env var naming: FABLEDNETMON_ + uppercased dotted path, dots -> __ This is the only config read from files/env vars at startup.
Example: database.url -> FABLEDNETMON_DATABASE__URL Everything else is stored in the app_settings DB table.
List values are never overridden by env vars.
config_path is optional — used only for backwards compatibility with
existing config.yaml deployments. The file is not required.
""" """
from dotenv import load_dotenv
load_dotenv() load_dotenv()
raw: dict[str, Any] = {}
if config_path is None: if config_path is None:
config_path = Path("config.yaml") config_path = Path("config.yaml")
config_path = Path(config_path) config_path = Path(config_path)
if config_path.exists(): if config_path.exists():
import yaml
with config_path.open() as f: with config_path.open() as f:
cfg: dict[str, Any] = yaml.safe_load(f) or {} raw = yaml.safe_load(f) or {}
else:
cfg = {}
# Apply env var overrides (scalars only) database_url = (
for key, value in os.environ.items(): os.environ.get("FABLEDNETMON_DATABASE_URL")
if not key.startswith(_PREFIX): or os.environ.get("FABLEDNETMON_DATABASE__URL")
continue or raw.get("database", {}).get("url")
path = key[len(_PREFIX):].lower().split("__") )
_set_nested(cfg, path, value) if not database_url:
raise ValueError(
"Database URL is required. Set FABLEDNETMON_DATABASE_URL env var "
"or add 'database.url' to config.yaml."
)
# Validate required keys secret_key = _resolve_secret_key(raw)
for dotted in REQUIRED_KEYS: plugin_dir = (
parts = dotted.split(".") os.environ.get("FABLEDNETMON_PLUGIN_DIR")
node = cfg or raw.get("plugin_dir", "plugins")
for part in parts: )
if not isinstance(node, dict) or part not in node:
raise ValueError(
f"Required config key '{dotted}' is missing. "
f"Set it in config.yaml or via env var "
f"FABLEDNETMON_{dotted.upper().replace('.', '__')}"
)
node = node[part]
return cfg return {
"database_url": database_url,
"secret_key": secret_key,
"plugin_dir": plugin_dir,
}
def _set_nested(cfg: dict, path: list[str], value: str) -> None: def _resolve_secret_key(raw: dict) -> str:
"""Set a scalar value at a nested dict path. Skips if target is a list.""" """Resolve secret_key: env var → file → auto-generate."""
node = cfg from_env = os.environ.get("FABLEDNETMON_SECRET_KEY") or raw.get("secret_key")
for part in path[:-1]: if from_env:
node = node.setdefault(part, {}) return from_env
key = path[-1]
existing = node.get(key) if _SECRET_KEY_FILE.exists():
if isinstance(existing, list): key = _SECRET_KEY_FILE.read_text().strip()
return # never override list values if key:
node[key] = value return key
key = secrets.token_hex(32)
try:
_SECRET_KEY_FILE.parent.mkdir(parents=True, exist_ok=True)
_SECRET_KEY_FILE.write_text(key)
logger.info("Generated new secret key and saved to %s", _SECRET_KEY_FILE)
except OSError as exc:
logger.warning(
"Could not write secret key to %s (%s). "
"Key will not persist across restarts.",
_SECRET_KEY_FILE, exc,
)
return key