v0.8.14-beta - Raspberry Pi compatibiliteit en UniFi Mongo herstellen
All checks were successful
Deploy server-up (dev) / deploy (push) Successful in 5m5s
All checks were successful
Deploy server-up (dev) / deploy (push) Successful in 5m5s
This commit is contained in:
parent
6cc078e7b9
commit
1575db42c2
24 changed files with 408 additions and 20 deletions
31
CHANGELOG.md
31
CHANGELOG.md
|
|
@ -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
|
||||
|
||||
**Bijwerken vanuit de interface vroeg Docker om een image dat niet bestond.**
|
||||
|
|
|
|||
2
VERSION
2
VERSION
|
|
@ -1 +1 @@
|
|||
0.8.13-beta
|
||||
0.8.14-beta
|
||||
|
|
|
|||
|
|
@ -18,7 +18,11 @@
|
|||
"categories": [
|
||||
"productiviteit",
|
||||
"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": [
|
||||
{
|
||||
|
|
|
|||
|
|
@ -17,7 +17,11 @@
|
|||
},
|
||||
"categories": [
|
||||
"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": [
|
||||
{
|
||||
|
|
|
|||
|
|
@ -18,7 +18,11 @@
|
|||
"categories": [
|
||||
"gezin",
|
||||
"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": [
|
||||
{
|
||||
|
|
|
|||
|
|
@ -17,7 +17,11 @@
|
|||
},
|
||||
"categories": [
|
||||
"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": [
|
||||
{
|
||||
|
|
|
|||
|
|
@ -18,7 +18,11 @@
|
|||
"categories": [
|
||||
"productiviteit",
|
||||
"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": [
|
||||
{
|
||||
|
|
|
|||
|
|
@ -18,7 +18,11 @@
|
|||
"categories": [
|
||||
"communicatie",
|
||||
"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": [
|
||||
{
|
||||
|
|
|
|||
|
|
@ -18,7 +18,11 @@
|
|||
"categories": [
|
||||
"media",
|
||||
"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": [
|
||||
{
|
||||
|
|
|
|||
|
|
@ -18,7 +18,11 @@
|
|||
"categories": [
|
||||
"fotos",
|
||||
"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": [
|
||||
{
|
||||
|
|
|
|||
|
|
@ -18,7 +18,11 @@
|
|||
"categories": [
|
||||
"media",
|
||||
"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": [
|
||||
{
|
||||
|
|
|
|||
|
|
@ -18,7 +18,11 @@
|
|||
"categories": [
|
||||
"netwerk",
|
||||
"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": [
|
||||
{
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ services:
|
|||
- MONGO_HOST=<< service_name >>-mongo
|
||||
- MONGO_PORT=27017
|
||||
- MONGO_DBNAME=unifi
|
||||
- MONGO_AUTHSOURCE=admin
|
||||
volumes:
|
||||
- << appdata_dir >>/<< service_name >>/config:/config
|
||||
ports:
|
||||
|
|
@ -19,18 +20,31 @@ services:
|
|||
- "<< port_stun >>:3478/udp"
|
||||
- "<< port_discovery >>:10001/udp"
|
||||
depends_on:
|
||||
- << service_name >>-mongo
|
||||
<< service_name >>-mongo:
|
||||
condition: service_healthy
|
||||
restart: unless-stopped
|
||||
|
||||
<< service_name >>-mongo:
|
||||
image: mongo:4.4
|
||||
image: mongo:4.4.29
|
||||
container_name: << service_name >>-mongo
|
||||
environment:
|
||||
- MONGO_INITDB_ROOT_USERNAME=unifi
|
||||
- MONGO_INITDB_ROOT_USERNAME=root
|
||||
- MONGO_INITDB_ROOT_PASSWORD=<< mongo_password >>
|
||||
- MONGO_INITDB_DATABASE=unifi
|
||||
- MONGO_USER=unifi
|
||||
- MONGO_PASS=<< mongo_password >>
|
||||
- MONGO_DBNAME=unifi
|
||||
- MONGO_AUTHSOURCE=admin
|
||||
volumes:
|
||||
- << 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
|
||||
|
||||
volumes:
|
||||
|
|
|
|||
24
apps/unifi-network/files/init-mongo.sh
Executable file
24
apps/unifi-network/files/init-mongo.sh
Executable 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 %>
|
||||
|
|
@ -19,7 +19,12 @@
|
|||
"categories": [
|
||||
"netwerk",
|
||||
"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": [
|
||||
{
|
||||
|
|
|
|||
|
|
@ -18,7 +18,11 @@
|
|||
"categories": [
|
||||
"beveiliging",
|
||||
"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": [
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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 |
|
||||
| `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 |
|
||||
| `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
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
if not repo:
|
||||
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:
|
||||
|
|
@ -1655,6 +1658,7 @@ def api_store_preview():
|
|||
repo_id=rid,
|
||||
format="boilerplate",
|
||||
metadata=meta,
|
||||
compatibility=boilerplates.compatibility(meta),
|
||||
fields=flds,
|
||||
services=diensten,
|
||||
depends_on=_afhankelijkheden_met_status(meta),
|
||||
|
|
@ -1804,6 +1808,11 @@ def api_store_install():
|
|||
is_bp = boilerplates.is_boilerplate(src)
|
||||
if not is_bp and not docker.has_compose(src):
|
||||
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
|
||||
# een lege sleutel levert een container op die niet start of die met een
|
||||
|
|
|
|||
|
|
@ -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).
|
||||
"""
|
||||
from __future__ import annotations
|
||||
import json, re, shutil
|
||||
import json, platform, re, shutil
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
|
|
@ -72,9 +72,60 @@ def metadata(d: Path) -> dict:
|
|||
"format": "boilerplate",
|
||||
"draft": bool(md.get("draft")),
|
||||
"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]:
|
||||
"""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
|
||||
rendered = _tidy(rendered)
|
||||
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:
|
||||
shutil.copy2(p, out)
|
||||
written.append(str(rel))
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
<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>
|
||||
<!-- Draait deze app al? Dat stond eerder als een klein
|
||||
vinkje naast de knop, waar niemand het zag. -->
|
||||
|
|
@ -684,6 +691,8 @@ tailwind.config = {
|
|||
</div>
|
||||
<div class="flex gap-2 mt-auto items-center">
|
||||
<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'">
|
||||
<span class="mdi mdi-download-outline"></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">
|
||||
<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
|
||||
blokkade: je kunt de broker of Home Assistant net zo goed op een
|
||||
andere machine hebben staan. -->
|
||||
|
|
@ -2181,6 +2197,7 @@ tailwind.config = {
|
|||
<span x-text="t('next')">Volgende</span> <span class="mdi mdi-chevron-right"></span>
|
||||
</button>
|
||||
<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()">
|
||||
<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>
|
||||
|
|
@ -3644,6 +3661,10 @@ function app() {
|
|||
},
|
||||
|
||||
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 = {
|
||||
mode: 'new',
|
||||
stack: stack.dir || stack.name, repo_id: rid,
|
||||
|
|
@ -3653,7 +3674,9 @@ function app() {
|
|||
envVars: [], images: [], envOverrides: {}, imageTags: {},
|
||||
fields: [], values: {}, preview_files: {},
|
||||
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.fieldInfo = null;
|
||||
|
|
@ -3663,6 +3686,13 @@ function app() {
|
|||
this.install.loading = false;
|
||||
if (!d.ok) { this.toast(d.msg || 'Preview faalde', 'er'); this.modal = null; return; }
|
||||
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') {
|
||||
this.install.fields = d.fields || [];
|
||||
const init = {};
|
||||
|
|
@ -3931,6 +3961,10 @@ function app() {
|
|||
},
|
||||
|
||||
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 = {
|
||||
stack: this.install.stack,
|
||||
repo_id: this.install.repo_id,
|
||||
|
|
|
|||
|
|
@ -349,6 +349,18 @@ check('balk staat halverwege', c.job.progress.pct === 50);
|
|||
// De poortcontrole praat met de backend, dus die checks zijn asynchroon.
|
||||
// Top-level await kan niet in een CommonJS-script; vandaar deze afsluiting.
|
||||
(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
|
||||
// image-tag zonder v. De updateknop moet de expliciete image-tag gebruiken.
|
||||
let updateBody = null;
|
||||
|
|
|
|||
|
|
@ -374,3 +374,39 @@ def test_huidige_versie_heeft_een_changelog_sectie():
|
|||
versie = (WORTEL / "VERSION").read_text(encoding="utf-8").strip()
|
||||
changelog = (WORTEL / "CHANGELOG.md").read_text(encoding="utf-8")
|
||||
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
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
"""Boilerplate-rendering: de sandbox, en de YAML-opruiming die conditionele
|
||||
blokken achterlaten."""
|
||||
import json
|
||||
import stat
|
||||
|
||||
import pytest
|
||||
|
||||
|
|
@ -103,3 +104,44 @@ def test_is_boilerplate_herkent_layout(tmp_path):
|
|||
src = _maak_boilerplate(tmp_path, "services: {}\n")
|
||||
assert bp.is_boilerplate(src)
|
||||
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
|
||||
|
|
|
|||
66
tests/test_platform_compat.py
Normal file
66
tests/test_platform_compat.py
Normal 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()
|
||||
Loading…
Reference in a new issue