platform/control-panel/tests/test_render.py
Bart Van Geyt fe4cbbe36a Phase 4: provisioning CLI (heleosctl)
Python control-panel package driving the full provisioning flow from a
site's site.yaml (docs/05):

- provision: ZFS web dataset + ownership, per-site DB + least-priv user,
  generated .env encrypted to secrets.enc.yaml (SOPS/age), render the
  profile templates + persist site.yaml, per-customer chrooted SFTP
  account, docker compose up.
- deprovision (gated: data destroyed only with --purge, after a final
  backup), backup (ZFS snapshot + mariadb-dump), restore (rollback +
  import), render (preview), list.

Design: one command/file runner with a real --dry-run (prints every
action, redacts secrets); idempotent steps; Config + Site validation
mirroring docs/03; passwords never logged.

Modules: cli, config, naming, context, render, runner, zfs, database,
secrets, sftp, compose, provision, backup. Plus pyproject (heleosctl
entry point), config.example.yaml, an example site, and a README.

Tests: 22 pure-logic unit tests (naming, config validation, template
render across all profiles + db on/off) — all passing. Full provision and
deprovision verified end-to-end in --dry-run.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 17:18:36 +02:00

66 lines
2.6 KiB
Python

from pathlib import Path
import yaml
from heleos import context, render
from heleos.config import Config, Site
REPO_ROOT = Path(__file__).resolve().parents[2]
TEMPLATES = REPO_ROOT / "site-templates" / "profiles"
def _cfg():
return Config(templates_dir=TEMPLATES, registry="reg.example.com/heleos")
def test_context_resolves_images_and_paths():
cfg = _cfg()
s = Site.from_dict({"customer": "acme", "site": "shop", "profile": "custom-php",
"domains": ["shop.acme.com"], "database": True})
ctx = context.build(cfg, s)
assert ctx["slug"] == "acme-shop"
assert ctx["slug_underscored"] == "acme_shop"
assert ctx["webroot"] == "/tank/customers/acme/shop/web"
assert ctx["php_image"] == "reg.example.com/heleos/php-fpm:8.3"
assert ctx["nginx_image"] == "reg.example.com/heleos/nginx:latest"
def test_custom_php_renders_valid_compose_with_db():
cfg = _cfg()
s = Site.from_dict({"customer": "acme", "site": "shop", "profile": "custom-php",
"domains": ["shop.acme.com", "www.shop.acme.com"], "database": True})
out = render.render(cfg, s)
doc = yaml.safe_load(out["docker-compose.yml"])
assert doc["name"] == "acme-shop"
assert set(doc["services"]) == {"nginx", "fpm"}
# fpm reaches the DB, so it is on the platform network.
assert "platform" in doc["services"]["fpm"]["networks"]
labels = doc["services"]["nginx"]["labels"]
rule = [l for l in labels if ".rule=" in l][0]
assert "Host(`shop.acme.com`) || Host(`www.shop.acme.com`)" in rule
def test_custom_php_without_db_has_no_platform_network():
cfg = _cfg()
s = Site.from_dict({"customer": "acme", "site": "api", "profile": "custom-php",
"domains": ["api.acme.com"], "database": False})
doc = yaml.safe_load(render.render(cfg, s)["docker-compose.yml"])
assert doc["services"]["fpm"]["networks"] == ["site"]
assert "platform" not in doc.get("networks", {})
def test_static_profile_is_nginx_only():
cfg = _cfg()
s = Site.from_dict({"customer": "acme", "site": "www", "profile": "static",
"domains": ["acme.com"]})
doc = yaml.safe_load(render.render(cfg, s)["docker-compose.yml"])
assert list(doc["services"]) == ["nginx"]
def test_redirect_profile_renders_redirect_conf():
cfg = _cfg()
s = Site.from_dict({"customer": "acme", "site": "old", "profile": "redirect",
"domains": ["old.acme.com"], "redirect_to": "https://acme.com",
"redirect_code": 302})
out = render.render(cfg, s)
assert "return 302 https://acme.com$request_uri;" in out["nginx-redirect.conf"]