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).
145 lines
4.8 KiB
Python
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
|