"""Provisioning orchestrator: ties ZFS, DB, secrets, render, SFTP and compose into the provision / deprovision flows described in docs/05-provisioning-workflow.md. """ from __future__ import annotations from pathlib import Path import yaml from . import backup, compose, database, render, secrets, sftp, zfs from .config import Config, Site from .runner import Runner SITE_FIELDS = ("customer", "site", "profile", "domains", "php_version", "database", "redirect_to", "redirect_code", "resources") def deployment_dir(cfg: Config, site: Site) -> Path: return cfg.deployments_dir / site.customer / site.site def _site_to_dict(site: Site) -> dict: return {f: getattr(site, f) for f in SITE_FIELDS} def provision(cfg: Config, site: Site, runner: Runner) -> dict: """Provision a site end-to-end. Idempotent where the underlying steps are.""" deploy_dir = deployment_dir(cfg, site) summary: dict = {"slug": site.slug, "deploy_dir": str(deploy_dir)} # 1. Storage zfs.create_web(runner, cfg, site) sftp.set_web_ownership(runner, cfg, site) # 2. Database + secrets (only for DB profiles) if site.database: db_password = secrets.generate_password() database.create_db_user(runner, cfg, site, db_password) secrets.write_env(runner, deploy_dir, {secrets.db_env_key(site): db_password}) secrets.encrypt_env(runner, cfg, deploy_dir) # 3. Render compose + nginx config + persist site.yaml (source of truth) for name, content in render.render(cfg, site).items(): runner.write_file(deploy_dir / name, content) runner.write_file(deploy_dir / "site.yaml", yaml.safe_dump(_site_to_dict(site), sort_keys=False)) # 4. SFTP (per customer; password only set when the account is first created) sftp_password = secrets.generate_password() created = sftp.ensure_customer_account(runner, cfg, site.customer, sftp_password) summary["sftp_user"] = f"sftp_{site.customer}" summary["sftp_password"] = sftp_password if created else None # 5. Deploy compose.up(runner, deploy_dir) return summary def deprovision(cfg: Config, site: Site, runner: Runner, *, purge: bool = False, final_backup: bool = True) -> dict: """Tear down a site. Data is destroyed only with ``purge=True`` and always after a final backup. """ deploy_dir = deployment_dir(cfg, site) summary: dict = {"slug": site.slug, "purged": purge} compose.down(runner, deploy_dir) if final_backup: summary["final_backup"] = backup.backup_site(runner, cfg, site) if site.database: database.drop_db_user(runner, cfg, site) if purge: # Destroy the site dataset (its /web child included) but keep the customer # dataset + SFTP account — other sites may still belong to this customer. zfs.destroy(runner, f"{cfg.pool}/customers/{site.customer}/{site.site}") runner.run(["rm", "-rf", str(deploy_dir)]) summary["data_destroyed"] = True else: summary["data_destroyed"] = False summary["note"] = "config/containers removed; data retained. Re-run with purge to delete." return summary