All checks were successful
Deploy server-up (dev) / deploy (push) Successful in 2m1s
- Elk *arr-onderdeel is nu ook een losse app in de store (46 nieuwe sjablonen); los en gebundeld komen uit één bron (tools/arr_sjablonen.py) met een test die bewaakt dat ze niet uiteen lopen - Alles deelt één /data-map, zodat hardlinks werken; losse /tv-, /movies- en /downloads-mounts zijn eruit, UMASK=002 erbij - Nieuwe eerste stap "Onderdelen" in het invulmenu, met kopjes per soort; de dubbele groepsschakelaar en de lege kaarten in Instellingen zijn weg - Eigen IP-adres per container in plaats van per stack; containers zonder adres houden hun poortmapping. Ook Pangolin publiceert nu per container - Huntarr vervangen door NeutArr (project offline na lekken, image bestaat niet meer), Maintainerr naar zijn nieuwe organisatie - 31 apps toegevoegd uit het *arr-ecosysteem, alle images tegen hun registry gecontroleerd - data_dir hernoemd naar appdata_dir in de hele catalogus, naast de nieuwe data_root; qBittorrent van poort 8080 naar 8097 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Q9eqpADJSRs49SoGGr4NAy
194 lines
7.5 KiB
Python
194 lines
7.5 KiB
Python
"""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"
|