"""Een eigen IP-adres per container, en een publieke URL per container. Beide gingen per stack. Bij een app met één container klopt dat, maar de ARR-stack heeft er vijftig: één adres voor de hele stack betekende dat de eerste container het kreeg en alle andere hun poortmappings kwijtraakten zonder er iets voor terug te krijgen. Daarna waren ze nergens meer te bereiken. """ import json import sys import time from pathlib import Path import pytest import yaml sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "server-up")) from conftest import login NETWERK = {"name": "lan", "parent": "eth0", "subnet": "192.168.1.0/24", "gateway": "192.168.1.1", "range": "192.168.1.240/28", "driver": "macvlan"} def _maak_bron(env, naam="tweeling"): """Sjabloon met twee containers, elk met een eigen poort.""" from core import git wortel = git.cache_dir("server-up") / "apps" / naam (wortel / "files").mkdir(parents=True, exist_ok=True) (wortel / "template.json").write_text(json.dumps({ "kind": "compose", "metadata": {"name": naam, "description": "test"}, "variables": [{"title": "Algemeen", "items": [ {"name": "service_name", "type": "str", "title": "Naam", "default": naam, "required": True}, ]}], }), encoding="utf-8") (wortel / "files" / "compose.yaml").write_text( "services:\n" " << service_name >>-een:\n" " image: nginx\n" " ports:\n" ' - "8801:80"\n' " << service_name >>-twee:\n" " image: nginx\n" " ports:\n" ' - "8802:80"\n', encoding="utf-8") return wortel @pytest.fixture def netwerk(env): import core as cfg cfg.patch({"NETWORKS": [NETWERK]}) def _installeer(client, csrf, monkeypatch, body): """Installeer zonder Docker: alleen het compose-bestand op schijf telt.""" from core import docker, networks import app as app_module monkeypatch.setattr(docker, "validate_compose", lambda *a, **k: (True, "")) monkeypatch.setattr(docker, "compose_up", lambda *a, **k: 0) monkeypatch.setattr(networks, "used_ips", lambda *a, **k: set()) monkeypatch.setattr(networks, "ensure_shared", lambda *a, **k: (True, "")) monkeypatch.setattr(app_module, "_reserved_ips", lambda: set()) r = client.post("/api/store/install", json=body, headers={"X-CSRF-Token": csrf}) jid = (r.get_json() or {}).get("job_id") if jid: einde = time.time() + 15 while time.time() < einde: d = client.get(f"/api/jobs/{jid}").get_json() or {} if d.get("status") in ("done", "error"): break time.sleep(0.05) return r def _compose(env, naam): import core as cfg d = Path(cfg.load()["LIBRARY_DIR"]) / naam return yaml.safe_load((d / "docker-compose.yml").read_text(encoding="utf-8")) def test_adres_per_container(client, env, netwerk, monkeypatch): csrf = login(client) _maak_bron(env) r = _installeer(client, csrf, monkeypatch, { "stack": "tweeling", "repo_id": "server-up", "instance": "tweeling", "network": "lan", "ips": {"tweeling-een": "192.168.1.241"}, "app_networks": [], }) assert r.get_json().get("job_id"), r.get_json() doc = _compose(env, "tweeling") een = doc["services"]["tweeling-een"] twee = doc["services"]["tweeling-twee"] assert een["networks"]["lan"] == {"ipv4_address": "192.168.1.241"} assert "ports" not in een # En dit is waar het om gaat: de andere container is níét stilzwijgend # onbereikbaar geworden. assert twee["ports"] == ["8802:80"] def test_leeg_adres_wordt_aangevuld(client, env, netwerk, monkeypatch): csrf = login(client) _maak_bron(env) _installeer(client, csrf, monkeypatch, { "stack": "tweeling", "repo_id": "server-up", "instance": "tweeling", "network": "lan", "ips": {"tweeling-een": "", "tweeling-twee": ""}, "app_networks": [], }) doc = _compose(env, "tweeling") adressen = {n: s["networks"]["lan"]["ipv4_address"] for n, s in doc["services"].items()} assert len(set(adressen.values())) == 2, f"zelfde adres twee keer: {adressen}" def test_adres_buiten_het_subnet_wordt_geweigerd(client, env, netwerk, monkeypatch): csrf = login(client) _maak_bron(env) r = _installeer(client, csrf, monkeypatch, { "stack": "tweeling", "repo_id": "server-up", "instance": "tweeling", "network": "lan", "ips": {"tweeling-een": "10.0.0.5"}, "app_networks": [], }) assert r.status_code == 400 assert "subnet" in r.get_json()["msg"] def test_adressen_staan_in_de_metadata(client, env, netwerk, monkeypatch): """Zonder dit weet het herconfigureerscherm niet meer welke container welk adres had.""" csrf = login(client) _maak_bron(env) _installeer(client, csrf, monkeypatch, { "stack": "tweeling", "repo_id": "server-up", "instance": "tweeling", "network": "lan", "ips": {"tweeling-een": "192.168.1.241"}, "app_networks": [], }) import core as cfg meta = json.loads((Path(cfg.load()["LIBRARY_DIR"]) / "tweeling" / ".serverup.json").read_text(encoding="utf-8")) assert meta["ips"] == {"tweeling-een": "192.168.1.241"} r = client.get("/api/stacks/tweeling/config") assert r.get_json()["ips"] == {"tweeling-een": "192.168.1.241"} def test_zonder_mapping_blijft_het_bij_een_adres(client, env, netwerk, monkeypatch): """Een app met één container hoort niets van deze uitbreiding te merken.""" csrf = login(client) _maak_bron(env) _installeer(client, csrf, monkeypatch, { "stack": "tweeling", "repo_id": "server-up", "instance": "tweeling", "network": "lan", "ip": "192.168.1.245", "app_networks": [], }) doc = _compose(env, "tweeling") met_adres = [n for n, s in doc["services"].items() if isinstance(s.get("networks"), dict) and "lan" in s["networks"]] assert met_adres == ["tweeling-een"] # ── Pangolin ───────────────────────────────────────────────────────────────── def test_publieke_url_per_container(client, env, netwerk, monkeypatch): """Pangolin pakte de eerste poort in het bestand. Bij twintig webinterfaces kreeg je dus twintig keer dezelfde.""" csrf = login(client) _maak_bron(env) _installeer(client, csrf, monkeypatch, { "stack": "tweeling", "repo_id": "server-up", "instance": "tweeling", "app_networks": [], }) from core import pangolin gezien = {} def nep(naam, subdomein, ip, poort, site_id=None, domein_id=""): gezien.update(naam=naam, ip=ip, poort=poort) return {"url": f"https://{subdomein}.example.com", "resource_id": 1} monkeypatch.setattr(pangolin, "ingesteld", lambda: True) monkeypatch.setattr(pangolin, "publiceer", nep) r = client.post("/api/stacks/tweeling/pangolin", json={"service": "tweeling-twee", "host_ip": "10.1.2.3"}, headers={"X-CSRF-Token": csrf}) assert r.get_json()["ok"], r.get_json() assert gezien["poort"] == 8802, gezien assert gezien["naam"] == "tweeling-twee" import core as cfg meta = json.loads((Path(cfg.load()["LIBRARY_DIR"]) / "tweeling" / ".serverup.json").read_text(encoding="utf-8")) assert meta["pangolin_urls"]["tweeling-twee"] == \ "https://tweeling-twee.example.com"