Files
recipe-maintainer/lib/models.py
autonomic-bot f283a371bb recipe-maintainer: public snapshot (secrets + deployment plans removed, single commit)
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).
2026-06-16 20:18:24 +00:00

145 lines
4.8 KiB
Python

"""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/<name>/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