v0.5.50-beta - containers tonen die niet bij een beheerde stack horen
All checks were successful
Deploy server-up (dev) / deploy (push) Successful in 1m5s
All checks were successful
Deploy server-up (dev) / deploy (push) Successful in 1m5s
Het dashboard meldde 13 actieve containers en 17 images terwijl die nergens terug te vinden waren. De tellers komen van `docker info` (de hele daemon), de stacklijst toont alleen mappen in LIBRARY_DIR met een compose-bestand. Alles wat buiten Server Up om gestart is telde dus wel mee maar was onzichtbaar. - Nieuw /api/containers: alle containers op de host, met per container of hij bij een beheerde stack hoort (compose-project of werkmap onder LIBRARY_DIR). - Blok "Overige containers" onder de stacklijst, met start/stop/herstart/logs. - Dashboardteller "actief" vermeldt nu hoeveel daarvan via Server Up loopt. - De eigen container is gemarkeerd en kan niet via dit blok gestopt worden; daarvoor blijft de herstartknop in de instellingen. - Rollen: een viewer ziet de lijst maar mag niets bedienen. Verholpen tijdens het bouwen: containers zonder poorten en zonder compose-labels vielen uit de lijst als ze als laatste regel stonden, doordat strip() de lege velden aan het eind wegnam en de regel daarna te weinig kolommen leek te hebben. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01C7oLCRYzY5ixJ5Sv8Y8EFb
This commit is contained in:
parent
34a48fb74a
commit
7f0d34cb1a
8 changed files with 348 additions and 7 deletions
36
CHANGELOG.md
36
CHANGELOG.md
|
|
@ -1,3 +1,39 @@
|
||||||
|
# v0.5.50-beta — Containers die nergens te vinden waren
|
||||||
|
|
||||||
|
Het dashboard meldde 13 actieve containers en 17 images, terwijl je die nergens
|
||||||
|
terugzag. Dat kwam doordat de cijfers en de lijst uit twee verschillende bronnen
|
||||||
|
komen:
|
||||||
|
|
||||||
|
* de **tellers** komen van `docker info` en gaan over de hele Docker-daemon
|
||||||
|
* de **stacklijst** toont alleen mappen in `LIBRARY_DIR` met een compose-bestand
|
||||||
|
|
||||||
|
Alles wat je buiten Server Up om had gestart — je Forgejo, de act_runner,
|
||||||
|
containers van vóór je Server Up ging gebruiken — telde dus wel mee maar was
|
||||||
|
onzichtbaar en onbedienbaar. Een beheertool die dingen telt die hij niet toont,
|
||||||
|
is verwarrend.
|
||||||
|
|
||||||
|
## Wat er nu is
|
||||||
|
|
||||||
|
- **Overzicht van alle containers op de host.** Onder de stacklijst staat een
|
||||||
|
blok "Overige containers" met alles wat niet bij een beheerde stack hoort,
|
||||||
|
inclusief image, poorten en compose-project. Starten, stoppen, herstarten en
|
||||||
|
logs bekijken kan direct.
|
||||||
|
- **De dashboardteller vertelt nu het hele verhaal**: onder "actief" staat
|
||||||
|
hoeveel daarvan via Server Up loopt.
|
||||||
|
- De eigen container is gemarkeerd als "deze app" en kan niet via dit blok
|
||||||
|
gestopt worden — daarvoor is de herstartknop in de instellingen, die dat
|
||||||
|
netjes afhandelt in plaats van zichzelf halverwege een verzoek te stoppen.
|
||||||
|
- Een viewer ziet de lijst wel maar kan niets bedienen.
|
||||||
|
|
||||||
|
## Verholpen tijdens het bouwen
|
||||||
|
|
||||||
|
Containers zonder poorten én zonder compose-labels vielen weg uit de lijst als
|
||||||
|
ze toevallig als laatste stonden: `strip()` haalde de lege velden aan het eind
|
||||||
|
van de uitvoer weg, waarna die regel te weinig kolommen leek te hebben. Gevonden
|
||||||
|
door de test die precies zo'n container als laatste zet.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
# v0.5.43-beta — Van-tot omrekenen naar een CIDR-blok
|
# v0.5.43-beta — Van-tot omrekenen naar een CIDR-blok
|
||||||
|
|
||||||
"Ik wil van 10.0.20.200 tot .254" is een volstrekt redelijke wens, maar bestaat
|
"Ik wil van 10.0.20.200 tot .254" is een volstrekt redelijke wens, maar bestaat
|
||||||
|
|
|
||||||
2
VERSION
2
VERSION
|
|
@ -1 +1 @@
|
||||||
0.5.46-beta
|
0.5.50-beta
|
||||||
|
|
|
||||||
|
|
@ -459,6 +459,75 @@ def api_update_rollback():
|
||||||
return jsonify(ok=True, job_id=jid, tag=st["previous_tag"])
|
return jsonify(ok=True, job_id=jid, tag=st["previous_tag"])
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/api/containers")
|
||||||
|
def api_containers():
|
||||||
|
"""Alle containers op deze host, met of ze bij een beheerde stack horen.
|
||||||
|
|
||||||
|
Het dashboard telde alles wat de Docker-daemon kent, terwijl de stacklijst
|
||||||
|
alleen toont wat in LIBRARY_DIR staat. Containers die je buiten Server Up om
|
||||||
|
hebt gestart — of die er al stonden — waren daardoor wel zichtbaar in de
|
||||||
|
tellers maar nergens terug te vinden.
|
||||||
|
"""
|
||||||
|
lib = Path(cfg.load()["LIBRARY_DIR"]).resolve()
|
||||||
|
beheerd = set()
|
||||||
|
if lib.is_dir():
|
||||||
|
beheerd = {d.name for d in lib.iterdir()
|
||||||
|
if d.is_dir() and not d.name.startswith(".") and docker.has_compose(d)}
|
||||||
|
|
||||||
|
containers = docker.list_containers()
|
||||||
|
for c in containers:
|
||||||
|
# Een container hoort bij ons als zijn compose-project overeenkomt met
|
||||||
|
# een map in de library, of als zijn projectmap daar onder valt.
|
||||||
|
stack = c.get("project") or ""
|
||||||
|
in_lib = False
|
||||||
|
if c.get("working_dir"):
|
||||||
|
try:
|
||||||
|
in_lib = Path(c["working_dir"]).resolve().parent == lib
|
||||||
|
except Exception:
|
||||||
|
in_lib = False
|
||||||
|
c["managed"] = bool(stack and stack in beheerd) or in_lib
|
||||||
|
c["stack"] = stack if c["managed"] else ""
|
||||||
|
c["self"] = c["name"] == CONTAINER_NAME
|
||||||
|
|
||||||
|
return jsonify(containers=containers,
|
||||||
|
managed=sum(1 for c in containers if c["managed"]),
|
||||||
|
total=len(containers),
|
||||||
|
running=sum(1 for c in containers if c["running"]))
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/api/containers/<name>/<action>", methods=["POST"])
|
||||||
|
def api_any_container_action(name, action):
|
||||||
|
"""Start/stop/herstart een willekeurige container op deze host.
|
||||||
|
|
||||||
|
Bedoeld voor containers die niet bij een beheerde stack horen. De eigen
|
||||||
|
container is uitgezonderd: daarvoor is /api/docker/restart, dat de
|
||||||
|
herstart netjes afhandelt in plaats van zichzelf midden in een verzoek te
|
||||||
|
stoppen.
|
||||||
|
"""
|
||||||
|
if action not in ("start", "stop", "restart"):
|
||||||
|
return jsonify(ok=False, msg="onbekende actie"), 400
|
||||||
|
if name == CONTAINER_NAME:
|
||||||
|
return jsonify(ok=False,
|
||||||
|
msg="Gebruik 'Container herstarten' in de instellingen "
|
||||||
|
"om Server Up zelf te herstarten."), 400
|
||||||
|
bestaand = {c["name"] for c in docker.list_containers()}
|
||||||
|
if name not in bestaand:
|
||||||
|
return jsonify(ok=False, msg="container niet gevonden"), 404
|
||||||
|
ok, msg = docker.container_action(name, action)
|
||||||
|
audit.log("containers", action, "ok" if ok else "error", ref=name,
|
||||||
|
ip=request.remote_addr)
|
||||||
|
return jsonify(ok=ok, msg=msg or ("gelukt" if ok else "mislukt"))
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/api/containers/<name>/logs")
|
||||||
|
def api_any_container_logs(name):
|
||||||
|
bestaand = {c["name"] for c in docker.list_containers()}
|
||||||
|
if name not in bestaand:
|
||||||
|
return jsonify(ok=False, msg="container niet gevonden"), 404
|
||||||
|
tail = min(max(int(request.args.get("tail", 200)), 1), 2000)
|
||||||
|
return jsonify(ok=True, logs=docker.container_logs(name, tail))
|
||||||
|
|
||||||
|
|
||||||
@app.route("/api/docker/images")
|
@app.route("/api/docker/images")
|
||||||
def api_images():
|
def api_images():
|
||||||
return jsonify(images=docker.images())
|
return jsonify(images=docker.images())
|
||||||
|
|
|
||||||
|
|
@ -137,6 +137,44 @@ def restart_container(name: str) -> tuple[bool, str]:
|
||||||
return r.returncode == 0, (r.stderr or r.stdout).strip()
|
return r.returncode == 0, (r.stderr or r.stdout).strip()
|
||||||
|
|
||||||
|
|
||||||
|
def list_containers(alle: bool = True) -> list[dict]:
|
||||||
|
"""Alle containers op deze host, met hun compose-herkomst.
|
||||||
|
|
||||||
|
De dashboardtellers komen van `docker info` en gaan over de hele daemon,
|
||||||
|
terwijl de stacklijst alleen toont wat in LIBRARY_DIR staat. Met deze lijst
|
||||||
|
kunnen we die twee met elkaar rijmen en ook containers tonen die Server Up
|
||||||
|
niet zelf heeft aangemaakt.
|
||||||
|
"""
|
||||||
|
fmt = ("{{.ID}}\t{{.Names}}\t{{.Image}}\t{{.State}}\t{{.Status}}\t{{.Ports}}"
|
||||||
|
"\t{{.Label \"com.docker.compose.project\"}}"
|
||||||
|
"\t{{.Label \"com.docker.compose.project.working_dir\"}}")
|
||||||
|
cmd = [_bin(), "ps", "--format", fmt]
|
||||||
|
if alle:
|
||||||
|
cmd.insert(2, "-a")
|
||||||
|
r = _run(cmd, timeout=20)
|
||||||
|
if r.returncode != 0:
|
||||||
|
return []
|
||||||
|
uit = []
|
||||||
|
# Bewust geen .strip() op de hele uitvoer: een container zonder poorten en
|
||||||
|
# zonder compose-labels eindigt op lege velden, en die tabs zou strip()
|
||||||
|
# weghalen — waarna de laatste regel te weinig kolommen leek te hebben en
|
||||||
|
# stilzwijgend werd overgeslagen.
|
||||||
|
for regel in r.stdout.splitlines():
|
||||||
|
if not regel.strip():
|
||||||
|
continue
|
||||||
|
d = regel.split("\t")
|
||||||
|
if len(d) < 4:
|
||||||
|
continue
|
||||||
|
d += [""] * (8 - len(d))
|
||||||
|
uit.append({
|
||||||
|
"id": d[0], "name": d[1], "image": d[2],
|
||||||
|
"state": d[3], "status": d[4], "ports": d[5],
|
||||||
|
"project": d[6], "working_dir": d[7],
|
||||||
|
"running": d[3].lower() in ("running", "up"),
|
||||||
|
})
|
||||||
|
return sorted(uit, key=lambda c: c["name"])
|
||||||
|
|
||||||
|
|
||||||
def container_action(name: str, action: str) -> tuple[bool, str]:
|
def container_action(name: str, action: str) -> tuple[bool, str]:
|
||||||
"""Start, stop of herstart één container.
|
"""Start, stop of herstart één container.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -199,10 +199,18 @@ tailwind.config = {
|
||||||
<!-- ══ Dashboard ══ -->
|
<!-- ══ Dashboard ══ -->
|
||||||
<section x-show="page === 'dashboard'" x-cloak class="anim">
|
<section x-show="page === 'dashboard'" x-cloak class="anim">
|
||||||
<div class="grid grid-cols-2 lg:grid-cols-4 gap-4 mb-7">
|
<div class="grid grid-cols-2 lg:grid-cols-4 gap-4 mb-7">
|
||||||
<div class="stat-card">
|
<button class="stat-card text-left hover:shadow-soft2 transition" @click="goto('stacks')">
|
||||||
<div class="stat-icon bg-ok/10 text-ok"><span class="mdi mdi-play-circle"></span></div>
|
<div class="stat-icon bg-ok/10 text-ok"><span class="mdi mdi-play-circle"></span></div>
|
||||||
<div class="min-w-0"><div class="stat-value" x-text="docker.running ?? '–'"></div><div class="stat-label" x-text="t('running')">Running</div></div>
|
<div class="min-w-0">
|
||||||
|
<div class="stat-value" x-text="docker.running ?? '–'"></div>
|
||||||
|
<div class="stat-label" x-text="t('running')">Running</div>
|
||||||
|
<!-- De teller komt van de Docker-daemon en telt dus ook containers
|
||||||
|
die niet door Server Up zijn aangemaakt. -->
|
||||||
|
<div class="text-[10px] text-dm dark:text-dm-dark mt-0.5"
|
||||||
|
x-show="hostContainers.total"
|
||||||
|
x-text="t('of_which_managed', {n: hostContainers.managed})"></div>
|
||||||
</div>
|
</div>
|
||||||
|
</button>
|
||||||
<div class="stat-card">
|
<div class="stat-card">
|
||||||
<div class="stat-icon bg-dm/15 text-dm dark:text-dm-dark"><span class="mdi mdi-stop-circle-outline"></span></div>
|
<div class="stat-icon bg-dm/15 text-dm dark:text-dm-dark"><span class="mdi mdi-stop-circle-outline"></span></div>
|
||||||
<div class="min-w-0"><div class="stat-value" x-text="(docker.containers ?? 0) - (docker.running ?? 0)"></div><div class="stat-label" x-text="t('stopped')">Stopped</div></div>
|
<div class="min-w-0"><div class="stat-value" x-text="(docker.containers ?? 0) - (docker.running ?? 0)"></div><div class="stat-label" x-text="t('stopped')">Stopped</div></div>
|
||||||
|
|
@ -326,6 +334,46 @@ tailwind.config = {
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
<div x-show="filteredStacks.length === 0" class="empty"><span class="mdi mdi-server-network"></span><p x-text="t('no_stacks')">Geen stacks gevonden</p></div>
|
<div x-show="filteredStacks.length === 0" class="empty"><span class="mdi mdi-server-network"></span><p x-text="t('no_stacks')">Geen stacks gevonden</p></div>
|
||||||
|
|
||||||
|
<!-- Containers die niet bij een door Server Up beheerde stack horen.
|
||||||
|
Zonder dit blok tellen ze wel mee op het dashboard maar zijn ze
|
||||||
|
nergens terug te vinden. -->
|
||||||
|
<div x-show="unmanaged.length" class="mt-8">
|
||||||
|
<div class="flex items-center gap-2 mb-2">
|
||||||
|
<span class="mdi mdi-cube-outline text-t2 dark:text-t2-dark"></span>
|
||||||
|
<h3 class="text-sm font-bold text-wh dark:text-wh-dark" x-text="t('other_containers')">Overige containers</h3>
|
||||||
|
<span class="badge badge-dim" x-text="unmanaged.length"></span>
|
||||||
|
<button class="btn-icon ml-auto w-9 h-9" @click="loadHostContainers()"><span class="mdi mdi-refresh"></span></button>
|
||||||
|
</div>
|
||||||
|
<p class="text-xs text-t2 dark:text-t2-dark mb-3" x-text="t('other_containers_hint')"></p>
|
||||||
|
<div class="card divide-y divide-bd dark:divide-bd-dark">
|
||||||
|
<template x-for="c in unmanaged" :key="c.id">
|
||||||
|
<div class="px-4 py-3 flex flex-wrap items-center gap-2">
|
||||||
|
<span class="w-2.5 h-2.5 rounded-full shrink-0" :class="c.running ? 'bg-ok' : 'bg-dm dark:bg-dm-dark'"></span>
|
||||||
|
<div class="min-w-0 flex-1">
|
||||||
|
<div class="text-sm font-semibold text-wh dark:text-wh-dark truncate" x-text="c.name"></div>
|
||||||
|
<div class="text-[10px] font-mono text-dm dark:text-dm-dark truncate">
|
||||||
|
<span x-text="c.image"></span><span x-show="c.ports"> · </span><span x-text="c.ports"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<span class="badge badge-dim" x-show="c.project" x-text="c.project"></span>
|
||||||
|
<span class="badge badge-info" x-show="c.self" x-text="t('this_app')">deze app</span>
|
||||||
|
<template x-if="!c.self">
|
||||||
|
<div class="flex gap-1.5">
|
||||||
|
<button class="btn-sm !h-8 !px-2" :title="c.running ? t('stop') : t('start')"
|
||||||
|
@click="hostContainerAction(c.name, c.running ? 'stop' : 'start')">
|
||||||
|
<span class="mdi" :class="c.running ? 'mdi-stop' : 'mdi-play'"></span>
|
||||||
|
</button>
|
||||||
|
<button class="btn-sm !h-8 !px-2" :title="t('restart')"
|
||||||
|
@click="hostContainerAction(c.name, 'restart')"><span class="mdi mdi-restart"></span></button>
|
||||||
|
<button class="btn-sm !h-8 !px-2" :title="t('logs')"
|
||||||
|
@click="openHostLogs(c.name)"><span class="mdi mdi-text-box-outline"></span></button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- ══ App Store ══ -->
|
<!-- ══ App Store ══ -->
|
||||||
|
|
@ -1362,6 +1410,9 @@ function app() {
|
||||||
// Uitklapbaar containerpaneel per stack: { <stack>: {open, loading, items} }
|
// Uitklapbaar containerpaneel per stack: { <stack>: {open, loading, items} }
|
||||||
cpanel: {},
|
cpanel: {},
|
||||||
|
|
||||||
|
// Containers op deze host, ook die Server Up niet zelf beheert
|
||||||
|
hostContainers: { containers: [], managed: 0, total: 0, running: 0 },
|
||||||
|
|
||||||
// Backups en updatestatus per stack
|
// Backups en updatestatus per stack
|
||||||
bkStack: '', bkList: [], bkSettings: {},
|
bkStack: '', bkList: [], bkSettings: {},
|
||||||
stackUpd: {}, checkingUpd: false,
|
stackUpd: {}, checkingUpd: false,
|
||||||
|
|
@ -1439,10 +1490,11 @@ function app() {
|
||||||
await this.loadDocker();
|
await this.loadDocker();
|
||||||
await this.loadModules();
|
await this.loadModules();
|
||||||
await this.loadDashboard();
|
await this.loadDashboard();
|
||||||
|
this.loadHostContainers();
|
||||||
this.loadUpdate();
|
this.loadUpdate();
|
||||||
setInterval(() => { if (this.page==='dashboard') this.loadDocker(); }, 10000);
|
setInterval(() => { if (this.page==='dashboard') this.loadDocker(); }, 10000);
|
||||||
this.$watch('page', async (p) => {
|
this.$watch('page', async (p) => {
|
||||||
if (p === 'stacks') { this.loadStacks(); this.loadStackUpdates(); }
|
if (p === 'stacks') { this.loadStacks(); this.loadStackUpdates(); this.loadHostContainers(); }
|
||||||
if (p === 'app_store') this.loadAppStore();
|
if (p === 'app_store') this.loadAppStore();
|
||||||
if (p === 'docker') this.loadImages();
|
if (p === 'docker') this.loadImages();
|
||||||
if (p === 'audit') this.loadAudit();
|
if (p === 'audit') this.loadAudit();
|
||||||
|
|
@ -1884,6 +1936,26 @@ function app() {
|
||||||
.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
|
.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// ── Containers op deze host ──────────────────────────────────────────────
|
||||||
|
async loadHostContainers() {
|
||||||
|
const d = await this.rpc('/api/containers');
|
||||||
|
this.hostContainers = d && d.containers ? d : { containers: [], managed: 0, total: 0, running: 0 };
|
||||||
|
},
|
||||||
|
get unmanaged() {
|
||||||
|
return (this.hostContainers.containers || []).filter(c => !c.managed);
|
||||||
|
},
|
||||||
|
async hostContainerAction(naam, actie) {
|
||||||
|
const d = await this.rpc(`/api/containers/${encodeURIComponent(naam)}/${actie}`, {});
|
||||||
|
this.toast(d.ok ? this.t(actie) + ' ✓' : (d.msg || this.t('error')), d.ok ? 'ok' : 'er');
|
||||||
|
await this.loadHostContainers();
|
||||||
|
this.loadDocker();
|
||||||
|
},
|
||||||
|
async openHostLogs(naam) {
|
||||||
|
const d = await this.rpc(`/api/containers/${encodeURIComponent(naam)}/logs`);
|
||||||
|
this.logs = { title: naam + ' · logs', content: d.logs || '(geen output)' };
|
||||||
|
this.modal = 'logs';
|
||||||
|
},
|
||||||
|
|
||||||
// ── Containers binnen een stack ──────────────────────────────────────────
|
// ── Containers binnen een stack ──────────────────────────────────────────
|
||||||
async toggleContainers(stack) {
|
async toggleContainers(stack) {
|
||||||
const cur = this.cpanel[stack] || { open: false, loading: false, items: [] };
|
const cur = this.cpanel[stack] || { open: false, loading: false, items: [] };
|
||||||
|
|
|
||||||
|
|
@ -242,5 +242,9 @@
|
||||||
"range_count": "{count} addresses",
|
"range_count": "{count} addresses",
|
||||||
"already_added": "already added",
|
"already_added": "already added",
|
||||||
"detect_empty": "No network with a default route was found on this server. Fill in the details below manually.",
|
"detect_empty": "No network with a default route was found on this server. Fill in the details below manually.",
|
||||||
"wizard_hint": "Run the wizard again to adjust paths and repositories. Your existing stacks are left untouched."
|
"wizard_hint": "Run the wizard again to adjust paths and repositories. Your existing stacks are left untouched.",
|
||||||
|
"of_which_managed": "of which {n} via Server Up",
|
||||||
|
"other_containers": "Other containers",
|
||||||
|
"other_containers_hint": "These containers run on this host but do not belong to a stack managed by Server Up — started manually, for example, or from before you began using Server Up. They do count towards the dashboard figures.",
|
||||||
|
"this_app": "this app"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -242,5 +242,9 @@
|
||||||
"range_count": "{count} adressen",
|
"range_count": "{count} adressen",
|
||||||
"already_added": "al toegevoegd",
|
"already_added": "al toegevoegd",
|
||||||
"detect_empty": "Er is geen netwerk met een default route gevonden op deze server. Vul de gegevens hieronder handmatig in.",
|
"detect_empty": "Er is geen netwerk met een default route gevonden op deze server. Vul de gegevens hieronder handmatig in.",
|
||||||
"wizard_hint": "Doorloop de wizard opnieuw om paden en repositories aan te passen. Je bestaande stacks blijven staan."
|
"wizard_hint": "Doorloop de wizard opnieuw om paden en repositories aan te passen. Je bestaande stacks blijven staan.",
|
||||||
|
"of_which_managed": "waarvan {n} via Server Up",
|
||||||
|
"other_containers": "Overige containers",
|
||||||
|
"other_containers_hint": "Deze containers draaien op deze host maar horen niet bij een stack die Server Up beheert — bijvoorbeeld handmatig gestart of van vóór je Server Up ging gebruiken. Ze tellen wel mee in de cijfers op het dashboard.",
|
||||||
|
"this_app": "deze app"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
118
tests/test_containers.py
Normal file
118
tests/test_containers.py
Normal file
|
|
@ -0,0 +1,118 @@
|
||||||
|
"""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
|
||||||
Loading…
Reference in a new issue