"""Data model for instances, deployments, and recipes. Provides dataclasses and loader functions that build on lib/config.py to give structured access to the project's configuration. """ from dataclasses import dataclass, field from lib import config # --------------------------------------------------------------------------- # Core dataclasses # --------------------------------------------------------------------------- @dataclass class Instance: """A test server where apps can be deployed.""" name: str # "b1cc" server: str # "b1cc.commoninternet.net" domain_suffix: str # "b1cc.commoninternet.net" protected_recipes: list[str] # ["traefik", "backup-bot-two"] def default_domain(self, recipe: str) -> str: """Compute the default domain for a recipe on this instance.""" return f"{recipe}.{self.domain_suffix}" @dataclass class Deployment: """A specific app deployment on an instance.""" recipe: str # "hedgedoc" domain: str # "hedgedoc.b1cc.commoninternet.net" instance: Instance name: str = "default" # "default" or custom name env_overrides: dict = field(default_factory=dict) @property def stack_prefix(self) -> str: """Docker stack name: domain with dots replaced by underscores.""" return self.domain.replace(".", "_") @property def server(self) -> str: return self.instance.server @dataclass class Recipe: """Metadata about a recipe we maintain.""" name: str dependencies: list[str] = field(default_factory=list) test_requires: list[str] = field(default_factory=list) sso_provider: str | None = None # "keycloak" | "authentik" | None setup_script: str | None = None post_deploy_script: str | None = None # --------------------------------------------------------------------------- # Loader functions # --------------------------------------------------------------------------- def load_instance(name: str) -> Instance: """Load an Instance from settings.toml.""" data = config.load_instance_toml(name) return Instance( name=data["name"], server=data.get("server", f"{name}.commoninternet.net"), domain_suffix=data.get("domain_suffix", f"{name}.commoninternet.net"), protected_recipes=data.get("protected_recipes", ["traefik", "backup-bot-two"]), ) def load_default_instance() -> Instance: """Load the default instance from settings.toml.""" name = config.get_default_instance_name() return load_instance(name) def list_instances() -> list[Instance]: """Load all configured instances.""" return [load_instance(name) for name in config.get_instance_names()] def load_recipe(name: str) -> Recipe: """Load a Recipe from recipe-info//recipe.toml.""" data = config.load_recipe_toml(name) deps_section = data.get("dependencies", {}) sso_section = data.get("sso", {}) post_deploy_section = data.get("post_deploy", {}) return Recipe( name=data.get("name", name), dependencies=deps_section.get("requires", []), test_requires=deps_section.get("test_requires", []), sso_provider=sso_section.get("provider"), setup_script=sso_section.get("setup_script"), post_deploy_script=post_deploy_section.get("script"), ) def load_deployment(recipe: str, *, instance: str | None = None, name: str = "default") -> Deployment: """Load a Deployment for a recipe on an instance. If instance is None, uses the default instance. Domain is computed as {recipe}.{instance.domain_suffix}. An optional 'domain' key in recipe.toml overrides this. """ if instance is None: inst = load_default_instance() else: inst = load_instance(instance) # Check recipe.toml for optional domain override recipe_data = config.load_recipe_toml(recipe) domain = recipe_data.get("domain", inst.default_domain(recipe)) return Deployment( recipe=recipe, domain=domain, instance=inst, name=name, ) def list_deployments(instance_name: str | None = None) -> list[Deployment]: """List all deployments by iterating recipe-info/*/recipe.toml files.""" if instance_name is None: inst = load_default_instance() instance_name = inst.name deployments = [] recipe_info_dir = config.paths.RECIPE_INFO_DIR if not recipe_info_dir.exists(): return [] for d in sorted(recipe_info_dir.iterdir()): recipe_toml = d / "recipe.toml" if d.is_dir() and recipe_toml.exists(): recipe_name = d.name dep = load_deployment(recipe_name, instance=instance_name) deployments.append(dep) return deployments