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).
This commit is contained in:
144
lib/models.py
Normal file
144
lib/models.py
Normal file
@ -0,0 +1,144 @@
|
||||
"""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
|
||||
Reference in New Issue
Block a user