v0.8.14-beta - Raspberry Pi compatibiliteit en UniFi Mongo herstellen
All checks were successful
Deploy server-up (dev) / deploy (push) Successful in 5m5s

This commit is contained in:
Ramon 2026-08-04 16:49:24 +02:00
parent 6cc078e7b9
commit 1575db42c2
24 changed files with 408 additions and 20 deletions

View file

@ -1,3 +1,34 @@
# v0.8.14-beta — Raspberry Pi krijgt werkende en eerlijke apps
**De UniFi-template startte een MongoDB-container, maar maakte de database-
gebruiker en rollen die UniFi nodig heeft niet aan.** Daardoor bleef de
controller zonder werkende database hangen. Tegelijk kon de app-store op een
Raspberry Pi ook images aanbieden die uitsluitend voor AMD64 bestaan; dat werd
pas zichtbaar nadat Docker de pull weigerde.
- **UniFi initialiseert MongoDB volledig.** Een init-script maakt de UniFi-
gebruiker met de voorgeschreven rollen aan, beide containers gebruiken
`authSource=admin` en UniFi wacht op de database-healthcheck.
- **Geschikt voor een 64-bits Raspberry Pi 4.** MongoDB staat vast op
`4.4.29`: deze tag heeft een ARM64-image en vermijdt de ARMv8.2-eis van
MongoDB 5 en hoger. De hoofdimage ondersteunt AMD64 en ARM64.
- **Platformcontrole vóór installatie.** Templates kunnen hun ondersteunde
Docker-architecturen vastleggen. De catalogus legt een mismatch uit, schakelt
de knop uit en de API weigert dezelfde installatie ook rechtstreeks.
- **Elf AMD64-only apps zijn gemarkeerd.** Alle 318 gebruikte image-manifests
zijn gecontroleerd. Cal.com, ComfyUI, Dawarich, Focalboard, Formbricks,
Mattermost, Pinchflat, Pixelfed, TubeArchivist, Unbound en Wazuh bevatten
momenteel minstens één image zonder ARM64-variant en worden daarom niet meer
als installeerbaar op een Pi getoond.
- **Init-scripts houden hun uitvoerrecht.** Tekstbestanden die tijdens het
renderen worden geschreven nemen voortaan de bestandsmodus van het sjabloon
over.
Een Mongo-init-script draait alleen bij een lege database. Server Up verwijdert
bestaande data daarom niet automatisch. Verwijder bij een eerdere mislukte
UniFi-installatie eerst bewust het oude Mongo-volume, of installeer onder een
nieuwe instantienaam.
# v0.8.13-beta — De update haalt nu de juiste release op # v0.8.13-beta — De update haalt nu de juiste release op
**Bijwerken vanuit de interface vroeg Docker om een image dat niet bestond.** **Bijwerken vanuit de interface vroeg Docker om een image dat niet bestond.**

View file

@ -1 +1 @@
0.8.13-beta 0.8.14-beta

View file

@ -18,7 +18,11 @@
"categories": [ "categories": [
"productiviteit", "productiviteit",
"zakelijk" "zakelijk"
] ],
"architectures": [
"amd64"
],
"architecture_note": "Een of meer containerimages van deze app worden alleen voor AMD64 gepubliceerd. Deze app werkt daardoor niet op een Raspberry Pi."
}, },
"variables": [ "variables": [
{ {

View file

@ -17,7 +17,11 @@
}, },
"categories": [ "categories": [
"ai" "ai"
] ],
"architectures": [
"amd64"
],
"architecture_note": "Een of meer containerimages van deze app worden alleen voor AMD64 gepubliceerd. Deze app werkt daardoor niet op een Raspberry Pi."
}, },
"variables": [ "variables": [
{ {

View file

@ -18,7 +18,11 @@
"categories": [ "categories": [
"gezin", "gezin",
"beveiliging" "beveiliging"
] ],
"architectures": [
"amd64"
],
"architecture_note": "Een of meer containerimages van deze app worden alleen voor AMD64 gepubliceerd. Deze app werkt daardoor niet op een Raspberry Pi."
}, },
"variables": [ "variables": [
{ {

View file

@ -17,7 +17,11 @@
}, },
"categories": [ "categories": [
"productiviteit" "productiviteit"
] ],
"architectures": [
"amd64"
],
"architecture_note": "Een of meer containerimages van deze app worden alleen voor AMD64 gepubliceerd. Deze app werkt daardoor niet op een Raspberry Pi."
}, },
"variables": [ "variables": [
{ {

View file

@ -18,7 +18,11 @@
"categories": [ "categories": [
"productiviteit", "productiviteit",
"zakelijk" "zakelijk"
] ],
"architectures": [
"amd64"
],
"architecture_note": "Een of meer containerimages van deze app worden alleen voor AMD64 gepubliceerd. Deze app werkt daardoor niet op een Raspberry Pi."
}, },
"variables": [ "variables": [
{ {

View file

@ -18,7 +18,11 @@
"categories": [ "categories": [
"communicatie", "communicatie",
"productiviteit" "productiviteit"
] ],
"architectures": [
"amd64"
],
"architecture_note": "Een of meer containerimages van deze app worden alleen voor AMD64 gepubliceerd. Deze app werkt daardoor niet op een Raspberry Pi."
}, },
"variables": [ "variables": [
{ {

View file

@ -18,7 +18,11 @@
"categories": [ "categories": [
"media", "media",
"downloaden" "downloaden"
] ],
"architectures": [
"amd64"
],
"architecture_note": "Een of meer containerimages van deze app worden alleen voor AMD64 gepubliceerd. Deze app werkt daardoor niet op een Raspberry Pi."
}, },
"variables": [ "variables": [
{ {

View file

@ -18,7 +18,11 @@
"categories": [ "categories": [
"fotos", "fotos",
"communicatie" "communicatie"
] ],
"architectures": [
"amd64"
],
"architecture_note": "Een of meer containerimages van deze app worden alleen voor AMD64 gepubliceerd. Deze app werkt daardoor niet op een Raspberry Pi."
}, },
"variables": [ "variables": [
{ {

View file

@ -18,7 +18,11 @@
"categories": [ "categories": [
"media", "media",
"downloaden" "downloaden"
] ],
"architectures": [
"amd64"
],
"architecture_note": "Een of meer containerimages van deze app worden alleen voor AMD64 gepubliceerd. Deze app werkt daardoor niet op een Raspberry Pi."
}, },
"variables": [ "variables": [
{ {

View file

@ -18,7 +18,11 @@
"categories": [ "categories": [
"netwerk", "netwerk",
"beveiliging" "beveiliging"
] ],
"architectures": [
"amd64"
],
"architecture_note": "Een of meer containerimages van deze app worden alleen voor AMD64 gepubliceerd. Deze app werkt daardoor niet op een Raspberry Pi."
}, },
"variables": [ "variables": [
{ {

View file

@ -11,6 +11,7 @@ services:
- MONGO_HOST=<< service_name >>-mongo - MONGO_HOST=<< service_name >>-mongo
- MONGO_PORT=27017 - MONGO_PORT=27017
- MONGO_DBNAME=unifi - MONGO_DBNAME=unifi
- MONGO_AUTHSOURCE=admin
volumes: volumes:
- << appdata_dir >>/<< service_name >>/config:/config - << appdata_dir >>/<< service_name >>/config:/config
ports: ports:
@ -19,18 +20,31 @@ services:
- "<< port_stun >>:3478/udp" - "<< port_stun >>:3478/udp"
- "<< port_discovery >>:10001/udp" - "<< port_discovery >>:10001/udp"
depends_on: depends_on:
- << service_name >>-mongo << service_name >>-mongo:
condition: service_healthy
restart: unless-stopped restart: unless-stopped
<< service_name >>-mongo: << service_name >>-mongo:
image: mongo:4.4 image: mongo:4.4.29
container_name: << service_name >>-mongo container_name: << service_name >>-mongo
environment: environment:
- MONGO_INITDB_ROOT_USERNAME=unifi - MONGO_INITDB_ROOT_USERNAME=root
- MONGO_INITDB_ROOT_PASSWORD=<< mongo_password >> - MONGO_INITDB_ROOT_PASSWORD=<< mongo_password >>
- MONGO_INITDB_DATABASE=unifi - MONGO_USER=unifi
- MONGO_PASS=<< mongo_password >>
- MONGO_DBNAME=unifi
- MONGO_AUTHSOURCE=admin
volumes: volumes:
- << service_name >>_mongodata:/data/db - << service_name >>_mongodata:/data/db
- ./init-mongo.sh:/docker-entrypoint-initdb.d/init-mongo.sh:ro
healthcheck:
test:
- CMD-SHELL
- 'mongo --quiet --username root --password "$${MONGO_INITDB_ROOT_PASSWORD}" --authenticationDatabase admin --eval "db.adminCommand({ ping: 1 })"'
interval: 10s
timeout: 5s
retries: 12
start_period: 30s
restart: unless-stopped restart: unless-stopped
volumes: volumes:

View file

@ -0,0 +1,24 @@
<% raw %>
#!/bin/bash
if which mongosh > /dev/null 2>&1; then
mongo_init_bin="mongosh"
else
mongo_init_bin="mongo"
fi
"${mongo_init_bin}" <<EOF
use ${MONGO_AUTHSOURCE}
db.auth("${MONGO_INITDB_ROOT_USERNAME}", "${MONGO_INITDB_ROOT_PASSWORD}")
db.createUser({
user: "${MONGO_USER}",
pwd: "${MONGO_PASS}",
roles: [
"clusterMonitor",
{ db: "${MONGO_DBNAME}", role: "dbOwner" },
{ db: "${MONGO_DBNAME}_stat", role: "dbOwner" },
{ db: "${MONGO_DBNAME}_audit", role: "dbOwner" },
{ db: "${MONGO_DBNAME}_restore", role: "dbOwner" }
]
})
EOF
<% endraw %>

View file

@ -19,7 +19,12 @@
"categories": [ "categories": [
"netwerk", "netwerk",
"beheer" "beheer"
] ],
"architectures": [
"amd64",
"arm64"
],
"architecture_note": "Op een Raspberry Pi is een 64-bits besturingssysteem vereist. Voor een Pi 4 is de database bewust vastgezet op MongoDB 4.4.29."
}, },
"variables": [ "variables": [
{ {

View file

@ -18,7 +18,11 @@
"categories": [ "categories": [
"beveiliging", "beveiliging",
"monitoring" "monitoring"
] ],
"architectures": [
"amd64"
],
"architecture_note": "Een of meer containerimages van deze app worden alleen voor AMD64 gepubliceerd. Deze app werkt daardoor niet op een Raspberry Pi."
}, },
"variables": [ "variables": [
{ {

View file

@ -112,6 +112,20 @@ De testsuite controleert dit voor elke app.
| `categories` | Eén of meer categorie-ids (zie onder). Ontbreken ze, dan worden ze afgeleid uit de tags | | `categories` | Eén of meer categorie-ids (zie onder). Ontbreken ze, dan worden ze afgeleid uit de tags |
| `icon` | `{"provider": "selfhst", "id": "…"}` of `dashboard-icons`. Bestaat het icoon niet, dan valt de interface terug op een emoji | | `icon` | `{"provider": "selfhst", "id": "…"}` of `dashboard-icons`. Bestaat het icoon niet, dan valt de interface terug op een emoji |
| `version` | Alleen ter informatie in de catalogus | | `version` | Alleen ter informatie in de catalogus |
| `architectures` | Optionele lijst met aantoonbaar ondersteunde Docker-architecturen: `amd64`, `arm64`, `arm/v7` of `arm/v6` |
| `architecture_note` | Uitleg die bij de platformmelding in de catalogus en het installatieformulier staat |
Laat `architectures` weg als de image-ondersteuning niet is geverifieerd. Een
expliciete mismatch blokkeert de installatie zowel in de interface als in de
API; ontbrekende of onbekende metadata blokkeert bewust niets. Voorbeeld:
```json
"architectures": ["amd64", "arm64"],
"architecture_note": "Op een Raspberry Pi is een 64-bits OS vereist."
```
Controleer alle images uit de compose-stack. Zodra een database- of
hulpcontainer alleen AMD64 aanbiedt, is de hele app alleen geschikt voor AMD64.
### Categorieën ### Categorieën

View file

@ -1391,7 +1391,10 @@ def api_repos_stacks(rid):
repo = next((r for r in cfg.load().get("APP_REPOS", []) if r["id"] == rid), None) repo = next((r for r in cfg.load().get("APP_REPOS", []) if r["id"] == rid), None)
if not repo: if not repo:
return jsonify(stacks=[]) return jsonify(stacks=[])
return jsonify(stacks=git.scan_stacks(rid, lib, subdir=repo.get("subdir", ""))) stacks = git.scan_stacks(rid, lib, subdir=repo.get("subdir", ""))
for stack in stacks:
stack["compatibility"] = boilerplates.compatibility(stack)
return jsonify(stacks=stacks)
def _is_stack_dir(p: Path) -> bool: def _is_stack_dir(p: Path) -> bool:
@ -1655,6 +1658,7 @@ def api_store_preview():
repo_id=rid, repo_id=rid,
format="boilerplate", format="boilerplate",
metadata=meta, metadata=meta,
compatibility=boilerplates.compatibility(meta),
fields=flds, fields=flds,
services=diensten, services=diensten,
depends_on=_afhankelijkheden_met_status(meta), depends_on=_afhankelijkheden_met_status(meta),
@ -1804,6 +1808,11 @@ def api_store_install():
is_bp = boilerplates.is_boilerplate(src) is_bp = boilerplates.is_boilerplate(src)
if not is_bp and not docker.has_compose(src): if not is_bp and not docker.has_compose(src):
return jsonify(ok=False, msg=f"'{stack}' heeft geen compose bestand") return jsonify(ok=False, msg=f"'{stack}' heeft geen compose bestand")
if is_bp:
passend = boilerplates.compatibility(boilerplates.metadata(src))
if not passend["supported"]:
return jsonify(ok=False, msg=passend["reason"],
compatibility=passend), 400
# Verplichte velden vóór het aanmaken van de job: een leeg wachtwoord of # Verplichte velden vóór het aanmaken van de job: een leeg wachtwoord of
# een lege sleutel levert een container op die niet start of die met een # een lege sleutel levert een container op die niet start of die met een

View file

@ -10,7 +10,7 @@ Renders boilerplate templates into a flat directory that Server Up can manage
just like any other stack i.e. a single compose file (+ optional .env). just like any other stack i.e. a single compose file (+ optional .env).
""" """
from __future__ import annotations from __future__ import annotations
import json, re, shutil import json, platform, re, shutil
from pathlib import Path from pathlib import Path
from typing import Any from typing import Any
@ -72,9 +72,60 @@ def metadata(d: Path) -> dict:
"format": "boilerplate", "format": "boilerplate",
"draft": bool(md.get("draft")), "draft": bool(md.get("draft")),
"depends_on": _afhankelijkheden(md.get("depends_on")), "depends_on": _afhankelijkheden(md.get("depends_on")),
"architectures": _architecturen(md.get("architectures")),
"architecture_note": str(md.get("architecture_note") or "").strip(),
} }
_ARCH_ALIASES = {
"x86_64": "amd64", "x86-64": "amd64", "amd64": "amd64",
"aarch64": "arm64", "arm64": "arm64", "arm64v8": "arm64",
"armv7l": "arm/v7", "armv7": "arm/v7", "arm/v7": "arm/v7",
"armv6l": "arm/v6", "armv6": "arm/v6", "arm/v6": "arm/v6",
}
def _architectuur(waarde: str) -> str:
return _ARCH_ALIASES.get(str(waarde or "").strip().lower(), "")
def _architecturen(waarde) -> list[str]:
"Normaliseer Docker-architecturen en negeer onbekende schrijfwijzen."
if isinstance(waarde, str):
waarde = [waarde]
uit: list[str] = []
for item in (waarde or []):
arch = _architectuur(item)
if arch and arch not in uit:
uit.append(arch)
return uit
def host_architecture(machine: str | None = None) -> str:
"Docker-architectuurnaam van de host waarop Server Up draait."
return _architectuur(machine if machine is not None else platform.machine())
def compatibility(meta: dict, machine: str | None = None) -> dict:
"Beoordeel expliciete template-metadata voor de huidige host."
toegestaan = _architecturen(meta.get("architectures"))
huidig = host_architecture(machine)
bekend = bool(toegestaan and huidig)
ondersteund = not bekend or huidig in toegestaan
reden = ""
if bekend and not ondersteund:
labels = {"amd64": "AMD64", "arm64": "ARM64",
"arm/v7": "ARM 32-bit", "arm/v6": "ARMv6"}
verwacht = ", ".join(labels.get(a, a) for a in toegestaan)
reden = (f"Deze app ondersteunt {labels.get(huidig, huidig)} niet. "
f"Ondersteund: {verwacht}.")
notitie = str(meta.get("architecture_note") or "").strip()
if notitie:
reden += f" {notitie}"
return {"known": bekend, "supported": ondersteund,
"architecture": huidig, "reason": reden}
def _afhankelijkheden(waarde) -> list[dict]: def _afhankelijkheden(waarde) -> list[dict]:
"""Welke andere apps heeft dit sjabloon nodig, en waarom? """Welke andere apps heeft dit sjabloon nodig, en waarom?
@ -437,6 +488,12 @@ def render_to_dir(src: Path, dest: Path, values: dict[str, Any]) -> list[str]:
# Drop blocks of pure whitespace left over after conditionals strip # Drop blocks of pure whitespace left over after conditionals strip
rendered = _tidy(rendered) rendered = _tidy(rendered)
out.write_text(rendered, encoding="utf-8") out.write_text(rendered, encoding="utf-8")
# Een tekstscript kan uitvoerbaar zijn. write_text maakt een nieuw
# bestand met standaardrechten; behoud daarom de bronmodus.
try:
shutil.copymode(p, out)
except OSError:
pass
else: else:
shutil.copy2(p, out) shutil.copy2(p, out)
written.append(str(rel)) written.append(str(rel))

View file

@ -650,6 +650,13 @@ tailwind.config = {
<div class="text-[11px] text-t2 dark:text-t2-dark line-clamp-2 leading-snug" x-text="stack.description"></div> <div class="text-[11px] text-t2 dark:text-t2-dark line-clamp-2 leading-snug" x-text="stack.description"></div>
</div> </div>
<span x-show="stack.format === 'boilerplate'" class="badge badge-info shrink-0" title="ChristianLempa Boilerplate">BP</span> <span x-show="stack.format === 'boilerplate'" class="badge badge-info shrink-0" title="ChristianLempa Boilerplate">BP</span>
<span x-show="(stack.architectures || []).includes('arm64')"
class="badge badge-ok shrink-0" title="Ondersteunt ARM64 en een Raspberry Pi met 64-bits OS">Pi 64-bit</span>
</div>
<div x-show="stack.compatibility && !stack.compatibility.supported"
class="rounded-lg border border-er/35 bg-er/5 text-er text-[11px] leading-snug px-2.5 py-2 mb-3 flex gap-2">
<span class="mdi mdi-alert-circle-outline shrink-0"></span>
<span x-text="stack.compatibility ? stack.compatibility.reason : ''"></span>
</div> </div>
<!-- Draait deze app al? Dat stond eerder als een klein <!-- Draait deze app al? Dat stond eerder als een klein
vinkje naast de knop, waar niemand het zag. --> vinkje naast de knop, waar niemand het zag. -->
@ -684,6 +691,8 @@ tailwind.config = {
</div> </div>
<div class="flex gap-2 mt-auto items-center"> <div class="flex gap-2 mt-auto items-center">
<button @click="openInstall(stack, repo.id)" class="btn-sm flex-1" <button @click="openInstall(stack, repo.id)" class="btn-sm flex-1"
:disabled="stack.compatibility && !stack.compatibility.supported"
:title="stack.compatibility && !stack.compatibility.supported ? stack.compatibility.reason : ''"
:class="(stack.instances || []).length ? '' : 'btn-primary'"> :class="(stack.instances || []).length ? '' : 'btn-primary'">
<span class="mdi mdi-download-outline"></span> <span class="mdi mdi-download-outline"></span>
<span x-text="(stack.instances || []).length ? t('install_again') : t('install')">Installeren</span> <span x-text="(stack.instances || []).length ? t('install_again') : t('install')">Installeren</span>
@ -1746,6 +1755,13 @@ tailwind.config = {
<template x-if="install.format === 'boilerplate' && !install.loading"> <template x-if="install.format === 'boilerplate' && !install.loading">
<div class="space-y-3"> <div class="space-y-3">
<div x-show="install.architecture_note"
class="rounded-xl border border-ac/35 bg-ac/5 px-3.5 py-3 flex items-start gap-2.5">
<span class="mdi mdi-raspberry-pi text-ac dark:text-ac-dark shrink-0"></span>
<span class="text-[11px] text-t2 dark:text-t2-dark leading-snug"
x-text="install.architecture_note"></span>
</div>
<!-- Wat deze app nodig heeft en nog niet draait. Advies, geen <!-- Wat deze app nodig heeft en nog niet draait. Advies, geen
blokkade: je kunt de broker of Home Assistant net zo goed op een blokkade: je kunt de broker of Home Assistant net zo goed op een
andere machine hebben staan. --> andere machine hebben staan. -->
@ -2181,6 +2197,7 @@ tailwind.config = {
<span x-text="t('next')">Volgende</span> <span class="mdi mdi-chevron-right"></span> <span x-text="t('next')">Volgende</span> <span class="mdi mdi-chevron-right"></span>
</button> </button>
<button class="btn btn-primary flex-1 sm:flex-none" x-show="!wizardActief() || opLaatsteStap()" <button class="btn btn-primary flex-1 sm:flex-none" x-show="!wizardActief() || opLaatsteStap()"
:disabled="install.mode !== 'edit' && install.compatibility && !install.compatibility.supported"
@click="install.mode === 'edit' ? doReconfigure() : doInstall()"> @click="install.mode === 'edit' ? doReconfigure() : doInstall()">
<span class="mdi" :class="install.mode === 'edit' ? 'mdi-content-save-outline' : 'mdi-rocket-launch'"></span> <span class="mdi" :class="install.mode === 'edit' ? 'mdi-content-save-outline' : 'mdi-rocket-launch'"></span>
<span x-text="install.mode === 'edit' ? t('save') : t('install')">Installeren</span> <span x-text="install.mode === 'edit' ? t('save') : t('install')">Installeren</span>
@ -3644,6 +3661,10 @@ function app() {
}, },
async openInstall(stack, rid) { async openInstall(stack, rid) {
if (stack.compatibility && !stack.compatibility.supported) {
this.toast(stack.compatibility.reason || 'Deze app werkt niet op deze architectuur.', 'er');
return;
}
this.install = { this.install = {
mode: 'new', mode: 'new',
stack: stack.dir || stack.name, repo_id: rid, stack: stack.dir || stack.name, repo_id: rid,
@ -3653,7 +3674,9 @@ function app() {
envVars: [], images: [], envOverrides: {}, imageTags: {}, envVars: [], images: [], envOverrides: {}, imageTags: {},
fields: [], values: {}, preview_files: {}, fields: [], values: {}, preview_files: {},
step: 0, tab: 'compose', editing: false, composeText: '', envText: '', step: 0, tab: 'compose', editing: false, composeText: '', envText: '',
network: '', ip: '', ips: {}, services: [], depends_on: [] network: '', ip: '', ips: {}, services: [], depends_on: [],
compatibility: { known: false, supported: true, architecture: '', reason: '' },
architecture_note: ''
}; };
this.modal = 'install'; this.modal = 'install';
this.fieldInfo = null; this.fieldInfo = null;
@ -3663,6 +3686,13 @@ function app() {
this.install.loading = false; this.install.loading = false;
if (!d.ok) { this.toast(d.msg || 'Preview faalde', 'er'); this.modal = null; return; } if (!d.ok) { this.toast(d.msg || 'Preview faalde', 'er'); this.modal = null; return; }
this.install.format = d.format || 'compose'; this.install.format = d.format || 'compose';
this.install.compatibility = d.compatibility || this.install.compatibility;
this.install.architecture_note = (d.metadata || {}).architecture_note || '';
if (!this.install.compatibility.supported) {
this.toast(this.install.compatibility.reason || 'Deze app werkt niet op deze architectuur.', 'er');
this.modal = null;
return;
}
if (d.format === 'boilerplate') { if (d.format === 'boilerplate') {
this.install.fields = d.fields || []; this.install.fields = d.fields || [];
const init = {}; const init = {};
@ -3931,6 +3961,10 @@ function app() {
}, },
async doInstall() { async doInstall() {
if (this.install.compatibility && !this.install.compatibility.supported) {
this.toast(this.install.compatibility.reason || 'Deze app werkt niet op deze architectuur.', 'er');
return;
}
const body = { const body = {
stack: this.install.stack, stack: this.install.stack,
repo_id: this.install.repo_id, repo_id: this.install.repo_id,

View file

@ -349,6 +349,18 @@ check('balk staat halverwege', c.job.progress.pct === 50);
// De poortcontrole praat met de backend, dus die checks zijn asynchroon. // De poortcontrole praat met de backend, dus die checks zijn asynchroon.
// Top-level await kan niet in een CommonJS-script; vandaar deze afsluiting. // Top-level await kan niet in een CommonJS-script; vandaar deze afsluiting.
(async () => { (async () => {
// Een expliciet ongeschikte architectuur opent zelfs het formulier niet.
let previewGevraagd = false;
let platformMelding = '';
c.rpc = async () => { previewGevraagd = true; return {ok: true}; };
c.toast = (m) => { platformMelding = m; };
await c.openInstall({
dir: 'arm-app',
compatibility: {supported: false, reason: 'Alleen een 64-bits OS.'}
}, 'server-up');
check('ongeschikte app opent geen installatie', previewGevraagd === false);
check('architectuurreden wordt getoond', platformMelding.includes('64-bits'));
// Forgejo noemt de release v0.8.13-beta, maar de registry-workflow pusht de // Forgejo noemt de release v0.8.13-beta, maar de registry-workflow pusht de
// image-tag zonder v. De updateknop moet de expliciete image-tag gebruiken. // image-tag zonder v. De updateknop moet de expliciete image-tag gebruiken.
let updateBody = null; let updateBody = null;

View file

@ -374,3 +374,39 @@ def test_huidige_versie_heeft_een_changelog_sectie():
versie = (WORTEL / "VERSION").read_text(encoding="utf-8").strip() versie = (WORTEL / "VERSION").read_text(encoding="utf-8").strip()
changelog = (WORTEL / "CHANGELOG.md").read_text(encoding="utf-8") changelog = (WORTEL / "CHANGELOG.md").read_text(encoding="utf-8")
assert f"# v{versie}" in changelog, f"geen changelog-sectie voor {versie}" assert f"# v{versie}" in changelog, f"geen changelog-sectie voor {versie}"
def test_unifi_heeft_pi_geschikte_en_correct_geinitialiseerde_mongodb():
app = WORTEL / "apps" / "unifi-network"
with tempfile.TemporaryDirectory() as t:
tmp = Path(t)
doc = _render(app, tmp)
script = (tmp / "init-mongo.sh").read_text(encoding="utf-8")
mongo = doc["services"]["unifi-mongo"]
unifi = doc["services"]["unifi"]
assert mongo["image"] == "mongo:4.4.29"
assert "./init-mongo.sh:/docker-entrypoint-initdb.d/init-mongo.sh:ro" in mongo["volumes"]
assert any(v == "MONGO_AUTHSOURCE=admin" for v in unifi["environment"])
assert any(v == "MONGO_AUTHSOURCE=admin" for v in mongo["environment"])
assert unifi["depends_on"]["unifi-mongo"]["condition"] == "service_healthy"
assert mongo["healthcheck"]["test"][0] == "CMD-SHELL"
assert isinstance(mongo["healthcheck"]["test"][1], str)
assert "$${MONGO_INITDB_ROOT_PASSWORD}" in mongo["healthcheck"]["test"][1]
assert "db.createUser" in script
assert "clusterMonitor" in script
assert 'role: "dbOwner"' in script
assert bp.metadata(app)["architectures"] == ["amd64", "arm64"]
AMD64_ALLEEN = {
"cal-com", "comfyui", "dawarich", "focalboard", "formbricks",
"mattermost", "pinchflat", "pixelfed", "tubearchivist", "unbound", "wazuh",
}
def test_catalogus_markeert_aantoonbaar_amd64_only_apps():
for naam in AMD64_ALLEEN:
meta = bp.metadata(WORTEL / "apps" / naam)
assert meta["architectures"] == ["amd64"], naam
assert "Raspberry Pi" in meta["architecture_note"], naam

View file

@ -1,6 +1,7 @@
"""Boilerplate-rendering: de sandbox, en de YAML-opruiming die conditionele """Boilerplate-rendering: de sandbox, en de YAML-opruiming die conditionele
blokken achterlaten.""" blokken achterlaten."""
import json import json
import stat
import pytest import pytest
@ -103,3 +104,44 @@ def test_is_boilerplate_herkent_layout(tmp_path):
src = _maak_boilerplate(tmp_path, "services: {}\n") src = _maak_boilerplate(tmp_path, "services: {}\n")
assert bp.is_boilerplate(src) assert bp.is_boilerplate(src)
assert not bp.is_boilerplate(tmp_path) assert not bp.is_boilerplate(tmp_path)
def test_architectuurmetadata_wordt_genormaliseerd(tmp_path):
src = _maak_boilerplate(tmp_path, "services: {}\n")
pad = src / "template.json"
data = json.loads(pad.read_text(encoding="utf-8"))
data["metadata"]["architectures"] = ["x86_64", "aarch64", "arm64", "onbekend"]
data["metadata"]["architecture_note"] = "Gebruik een 64-bits OS."
pad.write_text(json.dumps(data), encoding="utf-8")
meta = bp.metadata(src)
assert meta["architectures"] == ["amd64", "arm64"]
assert meta["architecture_note"] == "Gebruik een 64-bits OS."
def test_explicitiete_architectuur_mismatch_wordt_geweigerd():
meta = {"architectures": ["amd64", "arm64"],
"architecture_note": "Gebruik een 64-bits OS."}
assert bp.compatibility(meta, "aarch64")["supported"] is True
fout = bp.compatibility(meta, "armv7l")
assert fout["known"] is True
assert fout["supported"] is False
assert "ARM 32-bit" in fout["reason"]
assert "64-bits OS" in fout["reason"]
def test_onbekende_architectuurmetadata_blokkeert_niet():
assert bp.compatibility({}, "armv7l")["supported"] is True
assert bp.compatibility({"architectures": ["vreemd"]}, "armv7l")["supported"] is True
def test_render_behoudt_uitvoerbaar_recht_van_tekstscript(tmp_path):
src = _maak_boilerplate(tmp_path, "services: {}\n")
script = src / "files" / "init.sh"
script.write_text("#!/bin/sh\necho << service_name >>\n", encoding="utf-8")
script.chmod(0o755)
dest = tmp_path / "out"
bp.render_to_dir(src, dest, {"service_name": "test"})
assert (dest / "init.sh").stat().st_mode & stat.S_IXUSR

View file

@ -0,0 +1,66 @@
"""Architectuurcontrole voor apps met expliciete platformmetadata."""
import json
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "server-up"))
from conftest import login
def _arm_app(env):
from core import git
d = git.cache_dir("server-up") / "apps" / "alleen-arm64"
(d / "files").mkdir(parents=True, exist_ok=True)
(d / "template.json").write_text(json.dumps({
"kind": "compose",
"metadata": {
"name": "Alleen ARM64",
"description": "Testapp",
"tags": ["test"],
"architectures": ["arm64"],
"architecture_note": "Gebruik een 64-bits OS.",
},
"variables": [{"title": "Algemeen", "items": [{
"name": "service_name", "type": "str", "title": "Naam",
"default": "alleen-arm64", "required": True,
}]}],
}), encoding="utf-8")
(d / "files" / "compose.yaml").write_text(
"services:\n << service_name >>:\n image: nginx\n", encoding="utf-8")
return d
def test_preview_meldt_architectuur_mismatch(client, env, monkeypatch):
csrf = login(client)
_arm_app(env)
import app as app_module
monkeypatch.setattr(app_module.boilerplates.platform, "machine", lambda: "armv7l")
d = client.post("/api/store/preview", json={
"stack": "alleen-arm64", "repo_id": "server-up",
}, headers={"X-CSRF-Token": csrf}).get_json()
assert d["ok"] is True
assert d["compatibility"]["architecture"] == "arm/v7"
assert d["compatibility"]["supported"] is False
assert "64-bits OS" in d["compatibility"]["reason"]
def test_installatie_weigert_aantoonbaar_verkeerde_architectuur(
client, env, monkeypatch):
csrf = login(client)
_arm_app(env)
import app as app_module
monkeypatch.setattr(app_module.boilerplates.platform, "machine", lambda: "armv7l")
r = client.post("/api/store/install", json={
"stack": "alleen-arm64",
"repo_id": "server-up",
"instance": "past-niet",
"values": {"service_name": "past-niet"},
}, headers={"X-CSRF-Token": csrf})
assert r.status_code == 400
assert r.get_json()["compatibility"]["supported"] is False
assert not (env["lib"] / "past-niet").exists()