Sanitized single-commit public mirror of recipe-maintainer. - Removed test-ssh/.testenv (live creds); added test-ssh/.testenv.example placeholders. - Removed plans/ and planned-updates/ (deployment-planning docs) so no client/ deployment domains appear in the public repo. - All other secret stores were already gitignored. - docs.coopcloud.tech retained as a submodule (public upstream).
176 lines
4.9 KiB
Python
176 lines
4.9 KiB
Python
"""Central configuration for the recipe-maintainer-3 project.
|
|
|
|
Resolves the workspace root, provides paths to key directories,
|
|
and loads instance/deployment configuration from TOML files.
|
|
|
|
All paths are resolved lazily on first access and cached.
|
|
"""
|
|
|
|
import os
|
|
import tomllib
|
|
from pathlib import Path
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Workspace root detection
|
|
# ---------------------------------------------------------------------------
|
|
|
|
_workspace: Path | None = None
|
|
|
|
|
|
def get_workspace() -> Path:
|
|
"""Auto-detect the workspace root by walking up to find AGENTS.md."""
|
|
global _workspace
|
|
if _workspace is not None:
|
|
return _workspace
|
|
|
|
# Start from this file's location (lib/config.py → lib/ → workspace root)
|
|
candidate = Path(__file__).resolve().parent.parent
|
|
if (candidate / "AGENTS.md").exists():
|
|
_workspace = candidate
|
|
return _workspace
|
|
|
|
# Fallback: walk up from cwd
|
|
candidate = Path.cwd()
|
|
for _ in range(10):
|
|
if (candidate / "AGENTS.md").exists():
|
|
_workspace = candidate
|
|
return _workspace
|
|
parent = candidate.parent
|
|
if parent == candidate:
|
|
break
|
|
candidate = parent
|
|
|
|
raise RuntimeError(
|
|
"Could not find workspace root (no AGENTS.md found). "
|
|
"Are you running from within the recipe-maintainer-3 directory?"
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Path constants (lazy)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def _ws() -> Path:
|
|
return get_workspace()
|
|
|
|
|
|
class _Paths:
|
|
"""Lazy path accessors for key project directories."""
|
|
|
|
@property
|
|
def WORKSPACE(self) -> Path:
|
|
return _ws()
|
|
|
|
@property
|
|
def LIB_DIR(self) -> Path:
|
|
return _ws() / "lib"
|
|
|
|
@property
|
|
def SCRIPTS_DIR(self) -> Path:
|
|
return _ws() / "scripts"
|
|
|
|
@property
|
|
def RECIPE_INFO_DIR(self) -> Path:
|
|
return _ws() / "recipe-info"
|
|
|
|
@property
|
|
def TESTSECRETS_DIR(self) -> Path:
|
|
return _ws() / "recipe-info" / "testsecrets"
|
|
|
|
@property
|
|
def LOGS_DIR(self) -> Path:
|
|
return _ws() / "logs"
|
|
|
|
@property
|
|
def PLANS_DIR(self) -> Path:
|
|
return _ws() / "plans"
|
|
|
|
@property
|
|
def PLANNED_UPDATES_DIR(self) -> Path:
|
|
return _ws() / "planned-updates"
|
|
|
|
@property
|
|
def TERRAFORM_DIR(self) -> Path:
|
|
return _ws() / "terraform"
|
|
|
|
@property
|
|
def ABRA_DIR(self) -> Path:
|
|
env = os.environ.get("ABRA_DIR")
|
|
if env:
|
|
return Path(env)
|
|
return Path.home() / ".abra"
|
|
|
|
@property
|
|
def ABRA_RECIPES_DIR(self) -> Path:
|
|
return self.ABRA_DIR / "recipes"
|
|
|
|
@property
|
|
def ABRA_SERVERS_DIR(self) -> Path:
|
|
return self.ABRA_DIR / "servers"
|
|
|
|
|
|
paths = _Paths()
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Settings
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def load_settings() -> dict:
|
|
"""Load settings.toml from the workspace root."""
|
|
p = paths.WORKSPACE / "settings.toml"
|
|
if not p.exists():
|
|
return {}
|
|
with open(p, "rb") as f:
|
|
return tomllib.load(f)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Instance loading (from settings.toml [instances.*])
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def _load_instances_from_settings() -> dict:
|
|
"""Load the [instances] section from settings.toml."""
|
|
settings = load_settings()
|
|
return settings.get("instances", {})
|
|
|
|
|
|
def get_instance_names() -> list[str]:
|
|
"""Return all instance names from settings.toml [instances.*]."""
|
|
return list(_load_instances_from_settings().keys())
|
|
|
|
|
|
def get_default_instance_name() -> str:
|
|
"""Return the default instance from settings.toml."""
|
|
settings = load_settings()
|
|
default = settings.get("default_instance")
|
|
if not default:
|
|
raise RuntimeError("No default_instance set in settings.toml")
|
|
if default not in get_instance_names():
|
|
raise RuntimeError(
|
|
f"default_instance '{default}' from settings.toml "
|
|
f"not found in settings.toml [instances]"
|
|
)
|
|
return default
|
|
|
|
|
|
def load_instance_toml(name: str) -> dict:
|
|
"""Load raw TOML data for a specific instance from settings.toml."""
|
|
instances = _load_instances_from_settings()
|
|
if name not in instances:
|
|
raise ValueError(f"Instance '{name}' not found in settings.toml [instances]")
|
|
data = dict(instances[name])
|
|
data["name"] = name
|
|
return data
|
|
|
|
|
|
def load_recipe_toml(recipe_name: str) -> dict:
|
|
"""Load recipe-info/<recipe>/recipe.toml."""
|
|
p = paths.RECIPE_INFO_DIR / recipe_name / "recipe.toml"
|
|
if not p.exists():
|
|
return {"name": recipe_name}
|
|
with open(p, "rb") as f:
|
|
data = tomllib.load(f)
|
|
data.setdefault("name", recipe_name)
|
|
return data
|