259 lines
11 KiB
Python
259 lines
11 KiB
Python
"""Containers op de host: wat is beheerd en wat niet.
|
|
|
|
Het dashboard telt alles wat de Docker-daemon kent, terwijl de stacklijst alleen
|
|
toont wat in LIBRARY_DIR staat. Containers die daarbuiten vallen waren zichtbaar
|
|
in de cijfers maar nergens terug te vinden.
|
|
"""
|
|
import pytest
|
|
|
|
from conftest import login
|
|
|
|
PS_UITVOER = "\n".join([
|
|
"abc123\tvaultwarden\tvaultwarden/server:latest\trunning\tUp 2 hours\t0.0.0.0:8222->80/tcp\tvaultwarden\t/opt/serverup/stacks/vaultwarden",
|
|
"def456\tforgejo\tcodeberg.org/forgejo/forgejo:1.21\trunning\tUp 3 days\t0.0.0.0:3000->3000/tcp\tforgejo\t/opt/docker/forgejo",
|
|
"ghi789\tact_runner\tgitea/act_runner:latest\trunning\tUp 3 days\t\t\t",
|
|
"jkl012\tserver-up\tserver-up:0.5.46-beta\trunning\tUp 5 minutes\t127.0.0.1:5000->5000/tcp\tserver-up\t/opt/docker/server-up",
|
|
"mno345\toud-testje\tnginx:latest\texited\tExited (0) 4 days ago\t\t\t",
|
|
])
|
|
|
|
|
|
@pytest.fixture
|
|
def host(env, monkeypatch):
|
|
from core import docker
|
|
monkeypatch.setattr(docker, "_run", lambda *a, **k: type(
|
|
"R", (), {"returncode": 0, "stdout": PS_UITVOER, "stderr": ""})())
|
|
return docker
|
|
|
|
|
|
def test_list_containers_leest_alle_velden(host):
|
|
c = host.list_containers()
|
|
assert len(c) == 5
|
|
vw = next(x for x in c if x["name"] == "vaultwarden")
|
|
assert vw["image"] == "vaultwarden/server:latest"
|
|
assert vw["running"] is True
|
|
assert vw["project"] == "vaultwarden"
|
|
assert vw["working_dir"] == "/opt/serverup/stacks/vaultwarden"
|
|
|
|
|
|
def test_list_containers_zonder_labels(host):
|
|
"""Een container zonder compose-labels heeft lege project-/werkmapvelden."""
|
|
c = next(x for x in host.list_containers() if x["name"] == "act_runner")
|
|
assert c["project"] == "" and c["working_dir"] == ""
|
|
assert c["running"] is True
|
|
|
|
|
|
def test_gestopte_container_wordt_herkend(host):
|
|
c = next(x for x in host.list_containers() if x["name"] == "oud-testje")
|
|
assert c["running"] is False
|
|
|
|
|
|
def test_endpoint_scheidt_beheerd_van_de_rest(client, env, monkeypatch):
|
|
from core import docker
|
|
login(client)
|
|
# Eén stack in de library: vaultwarden.
|
|
stack = env["lib"] / "vaultwarden"
|
|
stack.mkdir(parents=True)
|
|
(stack / "docker-compose.yml").write_text("services:\n app:\n image: nginx\n")
|
|
monkeypatch.setattr(docker, "_run", lambda *a, **k: type(
|
|
"R", (), {"returncode": 0,
|
|
"stdout": PS_UITVOER.replace("/opt/serverup/stacks/vaultwarden",
|
|
str(stack)),
|
|
"stderr": ""})())
|
|
|
|
d = client.get("/api/containers").get_json()
|
|
beheerd = [c["name"] for c in d["containers"] if c["managed"]]
|
|
overig = [c["name"] for c in d["containers"] if not c["managed"]]
|
|
assert beheerd == ["vaultwarden"]
|
|
assert set(overig) == {"forgejo", "act_runner", "server-up", "oud-testje"}
|
|
assert d["managed"] == 1
|
|
assert d["total"] == 5
|
|
assert d["running"] == 4
|
|
|
|
|
|
def test_eigen_container_wordt_gemarkeerd(client, env, monkeypatch):
|
|
from core import docker
|
|
import app as app_module
|
|
login(client)
|
|
monkeypatch.setattr(app_module, "CONTAINER_NAME", "server-up")
|
|
monkeypatch.setattr(docker, "_run", lambda *a, **k: type(
|
|
"R", (), {"returncode": 0, "stdout": PS_UITVOER, "stderr": ""})())
|
|
d = client.get("/api/containers").get_json()
|
|
assert next(c for c in d["containers"] if c["name"] == "server-up")["self"] is True
|
|
|
|
|
|
def test_eigen_container_kan_niet_gestopt_worden(client, monkeypatch):
|
|
"""Zichzelf stoppen midden in een verzoek levert een halve actie op; daar
|
|
is de aparte herstartknop voor."""
|
|
import app as app_module
|
|
csrf = login(client)
|
|
monkeypatch.setattr(app_module, "CONTAINER_NAME", "server-up")
|
|
r = client.post("/api/containers/server-up/stop", headers={"X-CSRF-Token": csrf})
|
|
assert r.status_code == 400
|
|
assert "herstarten" in r.get_json()["msg"]
|
|
|
|
|
|
def test_onbekende_container(client, host):
|
|
csrf = login(client)
|
|
r = client.post("/api/containers/bestaatniet/stop", headers={"X-CSRF-Token": csrf})
|
|
assert r.status_code == 404
|
|
|
|
|
|
def test_onbekende_actie(client, host):
|
|
csrf = login(client)
|
|
r = client.post("/api/containers/forgejo/verwijder", headers={"X-CSRF-Token": csrf})
|
|
assert r.status_code == 400
|
|
|
|
|
|
def test_viewer_mag_geen_containers_bedienen(client, host):
|
|
csrf = login(client)
|
|
client.post("/api/auth/users",
|
|
json={"username": "kijker", "password": "eenlangwachtwoord",
|
|
"role": "viewer"},
|
|
headers={"X-CSRF-Token": csrf})
|
|
r = client.post("/api/auth/login",
|
|
json={"username": "kijker", "password": "eenlangwachtwoord"})
|
|
kijker_csrf = r.get_json()["csrf_token"]
|
|
assert client.get("/api/containers").status_code == 200
|
|
assert client.post("/api/containers/forgejo/stop",
|
|
headers={"X-CSRF-Token": kijker_csrf}).status_code == 403
|
|
|
|
|
|
# ── Een map zonder compose-bestand ───────────────────────────────────────────
|
|
# Die werd stilzwijgend overgeslagen: niet in de lijst, en de containers die
|
|
# erbij hoorden belandden onder "handmatig gestart". Je zag een leeg scherm en
|
|
# nergens stond waarom.
|
|
|
|
def test_map_zonder_compose_verdwijnt_niet_uit_de_lijst(client, env):
|
|
from conftest import login
|
|
login(client)
|
|
(env["lib"] / "kapotte-app").mkdir(parents=True, exist_ok=True)
|
|
|
|
d = client.get("/api/stacks").get_json()
|
|
namen = [s["name"] for s in d]
|
|
assert "kapotte-app" in namen, f"de map is niet zichtbaar: {namen}"
|
|
s = next(s for s in d if s["name"] == "kapotte-app")
|
|
assert s["has_compose"] is False, "hij doet alsof er compose is"
|
|
assert s["containers"] == []
|
|
assert s["running"] is False
|
|
|
|
|
|
def test_een_gewone_app_blijft_gewoon_staan(client, env, monkeypatch):
|
|
from conftest import login
|
|
from core import docker
|
|
monkeypatch.setattr(docker, "compose_ps", lambda *a, **k: [])
|
|
login(client)
|
|
d = env["lib"] / "echte-app"
|
|
d.mkdir(parents=True, exist_ok=True)
|
|
(d / "docker-compose.yml").write_text("services:\n web:\n image: nginx\n")
|
|
|
|
uit = client.get("/api/stacks").get_json()
|
|
s = next(s for s in uit if s["name"] == "echte-app")
|
|
assert s["has_compose"] is True
|
|
|
|
|
|
# ── Overnemen ────────────────────────────────────────────────────────────────
|
|
# Verwijderen laat een handmatig gestarte compose-app niet verdwijnen: hij staat
|
|
# er weer zodra dat project ergens anders start. Overnemen wél.
|
|
|
|
def _nep_container(monkeypatch, **velden):
|
|
from core import docker
|
|
c = {"id": "abc", "name": "losse-app", "image": "nginx", "state": "running",
|
|
"status": "Up", "ports": "", "project": "losse-app",
|
|
"working_dir": "", "running": True}
|
|
c.update(velden)
|
|
monkeypatch.setattr(docker, "list_containers", lambda *a, **k: [c])
|
|
return c
|
|
|
|
|
|
def test_overnemen_maakt_een_verwijzing_en_verplaatst_niets(client, env, monkeypatch, tmp_path):
|
|
from conftest import login
|
|
csrf = login(client)
|
|
bron = tmp_path / "eigen-map"
|
|
bron.mkdir()
|
|
(bron / "docker-compose.yml").write_text("services:\n web:\n image: nginx\n")
|
|
_nep_container(monkeypatch, working_dir=str(bron))
|
|
|
|
r = client.post("/api/containers/losse-app/adopt", headers={"X-CSRF-Token": csrf})
|
|
assert r.status_code == 200, r.get_json()
|
|
|
|
doel = env["lib"] / "losse-app"
|
|
assert doel.is_symlink(), "er is geen verwijzing gemaakt"
|
|
assert doel.resolve() == bron.resolve()
|
|
assert (bron / "docker-compose.yml").is_file(), "de bron is aangeraakt"
|
|
# En hij duikt op in de appslijst.
|
|
from core import docker
|
|
monkeypatch.setattr(docker, "compose_ps", lambda *a, **k: [])
|
|
namen = [s["name"] for s in client.get("/api/stacks").get_json()]
|
|
assert "losse-app" in namen, namen
|
|
|
|
|
|
def test_overnemen_van_een_map_waar_we_niet_bij_kunnen(client, env, monkeypatch):
|
|
"""De container draait op de host; wij zien alleen wat gekoppeld is."""
|
|
from conftest import login
|
|
csrf = login(client)
|
|
_nep_container(monkeypatch, working_dir="/ergens/op/de/host")
|
|
r = client.post("/api/containers/losse-app/adopt", headers={"X-CSRF-Token": csrf})
|
|
assert r.status_code == 400
|
|
assert "niet in de container gekoppeld" in r.get_json()["msg"]
|
|
|
|
|
|
def test_overnemen_zonder_compose_labels(client, env, monkeypatch):
|
|
from conftest import login
|
|
csrf = login(client)
|
|
_nep_container(monkeypatch, project="", working_dir="")
|
|
r = client.post("/api/containers/losse-app/adopt", headers={"X-CSRF-Token": csrf})
|
|
assert r.status_code == 400
|
|
assert "niet met docker compose gestart" in r.get_json()["msg"]
|
|
|
|
|
|
def test_overnemen_botst_niet_met_een_bestaande_app(client, env, monkeypatch, tmp_path):
|
|
from conftest import login
|
|
csrf = login(client)
|
|
(env["lib"] / "losse-app").mkdir(parents=True)
|
|
bron = tmp_path / "eigen-map"
|
|
bron.mkdir()
|
|
(bron / "docker-compose.yml").write_text("services: {}\n")
|
|
_nep_container(monkeypatch, working_dir=str(bron))
|
|
r = client.post("/api/containers/losse-app/adopt", headers={"X-CSRF-Token": csrf})
|
|
assert r.status_code == 409
|
|
assert "al een app" in r.get_json()["msg"]
|
|
|
|
|
|
def test_een_losse_container_mag_verwijderd_worden(client, env, monkeypatch):
|
|
from conftest import login
|
|
from core import docker
|
|
csrf = login(client)
|
|
_nep_container(monkeypatch)
|
|
gedaan = []
|
|
monkeypatch.setattr(docker, "container_action",
|
|
lambda n, a: (gedaan.append((n, a)), (True, "weg"))[1])
|
|
r = client.post("/api/containers/losse-app/remove", headers={"X-CSRF-Token": csrf})
|
|
assert r.status_code == 200, r.get_json()
|
|
assert gedaan == [("losse-app", "remove")]
|
|
|
|
|
|
def test_server_up_verwijdert_zichzelf_niet(client, env, monkeypatch):
|
|
from conftest import login
|
|
import app as app_mod
|
|
csrf = login(client)
|
|
_nep_container(monkeypatch, name=app_mod.CONTAINER_NAME)
|
|
r = client.post(f"/api/containers/{app_mod.CONTAINER_NAME}/remove",
|
|
headers={"X-CSRF-Token": csrf})
|
|
assert r.status_code == 400
|
|
|
|
|
|
def test_compose_ps_toont_alleen_werkelijk_gepubliceerde_poorten(host):
|
|
container = {
|
|
"Ports": "8443/tcp, 8843/tcp",
|
|
"Publishers": [
|
|
{"TargetPort": 8443, "PublishedPort": 8443, "Protocol": "tcp"},
|
|
{"TargetPort": 8443, "PublishedPort": 8443, "Protocol": "tcp"},
|
|
{"TargetPort": 8843, "PublishedPort": 0, "Protocol": "tcp"},
|
|
],
|
|
}
|
|
assert host._published_ports(container) == "8443->8443/tcp"
|
|
|
|
|
|
def test_compose_ps_valt_bij_oude_compose_terug_op_ports(host):
|
|
assert host._published_ports(
|
|
{"Ports": "0.0.0.0:8080->80/tcp"}) == "0.0.0.0:8080->80/tcp"
|