UmbrelApps: community app store whatsnext
Electrum Gate, Evolu Relay en Atreides Assets, met de image-recepten in tools/ en de tests in tests/. Begonnen zonder voorgeschiedenis op 09-10-2026. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
commit
85d85a1295
63 files changed
+13987
No files matched your search
@@ -0,0 +1,546 @@
|
||||
"""Toetst de vorm die umbrelOS van deze app store eist, voor élke app erin.
|
||||
|
||||
Waarom deze test bestaat. De twee bestaande testbestanden gaan over Electrum
|
||||
Gate; ze noemen die app-map bij naam. Sinds 25-08-2026 zit er een tweede app in
|
||||
deze repo en zijn er dus regels die niet over één app gaan maar over alle:
|
||||
|
||||
- het `id` in `umbrel-app.yml` moet gelijk zijn aan de mapnaam;
|
||||
- dat id moet beginnen met het store-id uit `umbrel-app-store.yml`;
|
||||
- de manifestvelden staan in de volgorde die de packaging-documentatie
|
||||
voorschrijft;
|
||||
- er is een `docker-compose.yml`, en de `app_proxy` daarin wijst naar een
|
||||
service die in datzelfde bestand bestaat.
|
||||
|
||||
Dat zijn precies de fouten die je op het apparaat pas merkt: de app verschijnt
|
||||
niet in de store, of hij verschijnt en start niet. Er komt geen nette
|
||||
foutmelding, want de runtime-validatie van het manifest staat in umbreld
|
||||
uitgecommentarieerd.
|
||||
|
||||
Wat deze toetsen expres NIET doen: eisen dat elke image op een digest gepind is.
|
||||
Dat is wél de regel, maar op 25-08-2026 haalt geen van de twee apps hem, en een
|
||||
suite die altijd rood staat wordt niet gelezen. Het staat als taak in de plannen
|
||||
Umbrelapp en Publicatie-Gate. Wat hier wel gebeurt is de pinstatus afdrukken, zodat
|
||||
je hem ziet zonder erover te struikelen. Wat wél getoetst wordt, sinds 07-09-2026:
|
||||
dat de tag van een eigen image gelijk is aan VERSION in het bijbehorende
|
||||
tools/<app>/build.sh, want een tag die nooit gebouwd is faalt pas op het apparaat.
|
||||
|
||||
Deze toetsen vinden hun apps zelf. Komt er een derde app bij, dan valt die
|
||||
automatisch onder alles hierboven en hoeft hier niets bij.
|
||||
|
||||
Draaien:
|
||||
|
||||
python tests/test_appstore_vorm.py
|
||||
"""
|
||||
|
||||
import sys
|
||||
|
||||
sys.dont_write_bytecode = True
|
||||
|
||||
import os # noqa: E402
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
REPO = os.path.abspath(os.path.join(HERE, os.pardir))
|
||||
STORE = os.path.join(REPO, "umbrel-app-store.yml")
|
||||
|
||||
# De volgorde uit de packaging-documentatie. Wat de spec niet noemt (icon,
|
||||
# backupIgnore, submitter, submission) mag erachter, niet ertussen.
|
||||
VOORGESCHREVEN = [
|
||||
"manifestVersion", "id", "category", "name", "version", "tagline",
|
||||
"description", "releaseNotes", "developer", "website", "dependencies",
|
||||
"repo", "support", "port", "gallery", "path",
|
||||
]
|
||||
|
||||
# Velden die er per se moeten staan, ook al is het schema soepeler: zonder deze
|
||||
# is de winkelpagina leeg of start de app niet.
|
||||
VERPLICHT = ["manifestVersion", "id", "category", "name", "version", "tagline",
|
||||
"description", "port"]
|
||||
|
||||
|
||||
class Uitslag:
|
||||
def __init__(self):
|
||||
self.goed = 0
|
||||
self.fout = []
|
||||
|
||||
def check(self, naam, gelukt, uitleg=""):
|
||||
if gelukt:
|
||||
self.goed += 1
|
||||
else:
|
||||
self.fout.append(naam + ((" - " + uitleg) if uitleg else ""))
|
||||
|
||||
def rapport(self):
|
||||
print()
|
||||
print("%d goed, %d fout" % (self.goed, len(self.fout)))
|
||||
for f in self.fout:
|
||||
print(" FOUT: " + f)
|
||||
return 0 if not self.fout else 1
|
||||
|
||||
|
||||
def lees(pad):
|
||||
with open(pad, "r", encoding="utf-8") as f:
|
||||
return f.read()
|
||||
|
||||
|
||||
def veld(tekst, naam):
|
||||
"""De waarde van een veld op het eerste niveau, of None.
|
||||
|
||||
Geen YAML-lezer: de suite heeft geen afhankelijkheden en dat is een
|
||||
projectregel. Daarom alleen velden die aan het begin van een regel staan.
|
||||
"""
|
||||
for regel in tekst.splitlines():
|
||||
if regel.startswith(naam + ":"):
|
||||
return regel.split(":", 1)[1].strip().strip('"').strip("'")
|
||||
return None
|
||||
|
||||
|
||||
def velden_in_volgorde(tekst):
|
||||
"""De veldnamen op het eerste niveau, in de volgorde waarin ze staan."""
|
||||
namen = []
|
||||
for regel in tekst.splitlines():
|
||||
if not regel or regel[0] in " \t#-":
|
||||
continue
|
||||
if ":" not in regel:
|
||||
continue
|
||||
namen.append(regel.split(":", 1)[0].strip())
|
||||
return namen
|
||||
|
||||
|
||||
def app_mappen():
|
||||
"""Elke map in de repo-root met een umbrel-app.yml erin."""
|
||||
gevonden = []
|
||||
for naam in sorted(os.listdir(REPO)):
|
||||
pad = os.path.join(REPO, naam)
|
||||
if not os.path.isdir(pad):
|
||||
continue
|
||||
if os.path.isfile(os.path.join(pad, "umbrel-app.yml")):
|
||||
gevonden.append(naam)
|
||||
return gevonden
|
||||
|
||||
|
||||
def test_er_is_een_store_met_apps(u, store_id, apps):
|
||||
u.check("umbrel-app-store.yml heeft een id", bool(store_id))
|
||||
u.check("en er is minstens een app-map gevonden", bool(apps),
|
||||
"geen enkele map met een umbrel-app.yml")
|
||||
|
||||
|
||||
def test_id_en_mapnaam(u, store_id, app):
|
||||
"""Mapnaam == id == store-prefix + rest. Alle drie of de app bestaat niet."""
|
||||
tekst = lees(os.path.join(REPO, app, "umbrel-app.yml"))
|
||||
app_id = veld(tekst, "id")
|
||||
|
||||
u.check("%s: het manifest heeft een id" % app, bool(app_id))
|
||||
if not app_id:
|
||||
return
|
||||
|
||||
u.check("%s: id is gelijk aan de mapnaam" % app, app_id == app,
|
||||
"manifest zegt %r" % app_id)
|
||||
u.check("%s: id begint met het store-id %r" % (app, store_id),
|
||||
app_id.startswith(store_id + "-"),
|
||||
"id is %r" % app_id)
|
||||
u.check("%s: id is lowercase kebab-case" % app,
|
||||
app_id == app_id.lower() and " " not in app_id and "_" not in app_id,
|
||||
"id is %r" % app_id)
|
||||
|
||||
|
||||
def test_verplichte_velden(u, app):
|
||||
tekst = lees(os.path.join(REPO, app, "umbrel-app.yml"))
|
||||
ontbreekt = [naam for naam in VERPLICHT if veld(tekst, naam) is None]
|
||||
u.check("%s: alle verplichte manifestvelden staan erin" % app,
|
||||
not ontbreekt, "ontbreekt: %r" % ontbreekt)
|
||||
|
||||
|
||||
def test_manifest_volgorde(u, app):
|
||||
"""De voorgeschreven velden in de voorgeschreven onderlinge orde.
|
||||
|
||||
Niet dat ze er alle zestien zijn: `dependencies` hoort weg te blijven bij een
|
||||
app zonder afhankelijkheden. Wel dat wat er staat niet door elkaar loopt, want
|
||||
dat is de eis waar een review op valt.
|
||||
"""
|
||||
aanwezig = velden_in_volgorde(lees(os.path.join(REPO, app, "umbrel-app.yml")))
|
||||
volgens_spec = [n for n in aanwezig if n in VOORGESCHREVEN]
|
||||
verwacht = [n for n in VOORGESCHREVEN if n in volgens_spec]
|
||||
|
||||
u.check("%s: de manifestvelden staan in de voorgeschreven volgorde" % app,
|
||||
volgens_spec == verwacht,
|
||||
"gevonden %r, verwacht %r" % (volgens_spec, verwacht))
|
||||
|
||||
# Wat de spec niet noemt hoort erachter, niet ertussen. Anders is de kop niet
|
||||
# letterlijk goed en moet er bij inlevering geschoven worden.
|
||||
laatste_spec = -1
|
||||
for i, naam in enumerate(aanwezig):
|
||||
if naam in VOORGESCHREVEN:
|
||||
laatste_spec = i
|
||||
extra_ertussen = [n for n in aanwezig[:laatste_spec] if n not in VOORGESCHREVEN]
|
||||
u.check("%s: velden buiten de spec staan achteraan" % app,
|
||||
not extra_ertussen, "ertussen: %r" % extra_ertussen)
|
||||
|
||||
|
||||
def recept_map(app):
|
||||
"""Waar het bouwrecept van een app staat: tools/<app-id zonder store-id>/.
|
||||
|
||||
Dat is de afspraak uit Werkafspraken.md: een Dockerfile hoort niet in de app-map,
|
||||
want die staat niet in de update-whitelist. De mapnaam is het app-id zonder
|
||||
het store-voorvoegsel, dus whatsnext-evolu-relay bouwt in tools/evolu-relay/.
|
||||
"""
|
||||
kort = app.split("-", 1)[1] if "-" in app else app
|
||||
return os.path.join(REPO, "tools", kort)
|
||||
|
||||
|
||||
def test_image_tag_is_de_gebouwde(u, app):
|
||||
"""De tag in de compose is VERSION uit build.sh, voor elke eigen image.
|
||||
|
||||
Een tag die in de compose staat maar nooit gebouwd is, faalt pas op het
|
||||
apparaat met "pull access denied". En andersom, een VERSION die verhoogd is
|
||||
zonder de compose mee te nemen, levert een nieuwe image en een oude
|
||||
installatie zonder dat iets dat meldt. Beide staan als regel in Werkafspraken.md;
|
||||
dit is de toets erop.
|
||||
|
||||
Alleen images uit het eigen recept: een vreemde image (postgres, nginx) heeft
|
||||
geen build.sh en valt hier buiten.
|
||||
"""
|
||||
build = os.path.join(recept_map(app), "build.sh")
|
||||
if not os.path.isfile(build):
|
||||
return
|
||||
|
||||
versie = None
|
||||
for regel in lees(build).splitlines():
|
||||
if regel.startswith("VERSION="):
|
||||
versie = regel.split("=", 1)[1].strip().strip('"')
|
||||
u.check("%s: build.sh heeft een VERSION" % app, bool(versie))
|
||||
if not versie:
|
||||
return
|
||||
|
||||
kort = os.path.basename(recept_map(app))
|
||||
compose = lees(os.path.join(REPO, app, "docker-compose.yml"))
|
||||
eigen = []
|
||||
for regel in compose.splitlines():
|
||||
kaal = regel.strip()
|
||||
if not kaal.startswith("image:"):
|
||||
continue
|
||||
image = kaal.split(":", 1)[1].strip()
|
||||
repo_en_tag = image.split("@", 1)[0]
|
||||
repo_naam = repo_en_tag.rsplit(":", 1)[0]
|
||||
if repo_naam.endswith("/" + kort):
|
||||
eigen.append(repo_en_tag)
|
||||
|
||||
u.check("%s: de compose gebruikt de eigen image" % app, bool(eigen),
|
||||
"geen image op .../%s gevonden" % kort)
|
||||
verkeerd = [i for i in eigen if not i.endswith(":" + versie)]
|
||||
u.check("%s: elke eigen image heeft tag %s uit build.sh" % (app, versie),
|
||||
not verkeerd, "gevonden: %r" % verkeerd)
|
||||
|
||||
|
||||
def test_compose_bestaat_en_hangt_samen(u, app):
|
||||
"""De app_proxy moet naar een service wijzen die bestaat.
|
||||
|
||||
Dit is een echte klasse fouten en niet een formaliteit: de hostnaam is
|
||||
`<app-id>_<service>_1`, dus hij bevat het app-id én de servicenaam. Verandert
|
||||
een van de twee, dan wijst de proxy naar niets en meldt umbrelOS alleen dat de
|
||||
server onbereikbaar is. Precies het symptoom dat bij Electrum Gate 0.0.3 een
|
||||
dag kostte.
|
||||
"""
|
||||
pad = os.path.join(REPO, app, "docker-compose.yml")
|
||||
u.check("%s: er is een docker-compose.yml" % app, os.path.isfile(pad))
|
||||
if not os.path.isfile(pad):
|
||||
return
|
||||
|
||||
tekst = lees(pad)
|
||||
|
||||
# Servicenamen: twee spaties diep onder services:, eindigend op een dubbele
|
||||
# punt. Geen YAML-lezer, zie veld().
|
||||
services = []
|
||||
in_services = False
|
||||
for regel in tekst.splitlines():
|
||||
if regel.startswith("services:"):
|
||||
in_services = True
|
||||
continue
|
||||
if in_services and regel and not regel[0].isspace():
|
||||
break
|
||||
if in_services and regel.startswith(" ") and not regel.startswith(" "):
|
||||
kaal = regel.strip()
|
||||
if kaal.endswith(":") and not kaal.startswith("#"):
|
||||
services.append(kaal[:-1])
|
||||
|
||||
u.check("%s: de compose heeft een app_proxy" % app,
|
||||
"app_proxy" in services, "services: %r" % services)
|
||||
|
||||
app_host = None
|
||||
for regel in tekst.splitlines():
|
||||
if regel.strip().startswith("APP_HOST:"):
|
||||
app_host = regel.split(":", 1)[1].strip()
|
||||
break
|
||||
|
||||
u.check("%s: de app_proxy heeft een APP_HOST" % app, bool(app_host))
|
||||
if not app_host:
|
||||
return
|
||||
|
||||
u.check("%s: APP_HOST begint met het app-id" % app,
|
||||
app_host.startswith(app + "_"),
|
||||
"APP_HOST is %r maar de map heet %r" % (app_host, app))
|
||||
|
||||
# De vorm is <app-id>_<service>_1; haal de servicenaam eruit en kijk of die
|
||||
# bestaat.
|
||||
rest = app_host[len(app) + 1:]
|
||||
u.check("%s: APP_HOST eindigt op _1" % app, rest.endswith("_1"),
|
||||
"APP_HOST is %r" % app_host)
|
||||
service = rest[:-2] if rest.endswith("_1") else rest
|
||||
u.check("%s: APP_HOST wijst naar een service die bestaat" % app,
|
||||
service in services,
|
||||
"wijst naar %r, aanwezig: %r" % (service, services))
|
||||
|
||||
|
||||
def test_data_onder_data(u, app):
|
||||
"""Wat de app bewaart staat onder data/, en er is een .gitkeep per map.
|
||||
|
||||
De appstore-eis is dat gebruikersstaat onder ${APP_DATA_DIR}/data/... valt en
|
||||
dat elke map die bij de eerste start moet bestaan in de repo staat. Een mount
|
||||
naar een map die er niet is, maakt Docker aan als root, en dan kan de app er
|
||||
niet in schrijven.
|
||||
"""
|
||||
pad = os.path.join(REPO, app, "docker-compose.yml")
|
||||
if not os.path.isfile(pad):
|
||||
return
|
||||
tekst = lees(pad)
|
||||
|
||||
buiten = []
|
||||
ontbreekt = []
|
||||
for regel in tekst.splitlines():
|
||||
kaal = regel.strip()
|
||||
if not kaal.startswith("- ${APP_DATA_DIR}/"):
|
||||
continue
|
||||
pad_in_app = kaal[len("- ${APP_DATA_DIR}/"):].split(":", 1)[0]
|
||||
# Losse bestanden die uit een *.template komen zijn geen gebruikersstaat.
|
||||
if "/" not in pad_in_app:
|
||||
continue
|
||||
if not pad_in_app.startswith("data/"):
|
||||
buiten.append(pad_in_app)
|
||||
continue
|
||||
op_schijf = os.path.join(REPO, app, pad_in_app)
|
||||
if not os.path.exists(op_schijf):
|
||||
ontbreekt.append(pad_in_app)
|
||||
elif os.path.isdir(op_schijf) and not os.path.isfile(
|
||||
os.path.join(op_schijf, ".gitkeep")):
|
||||
ontbreekt.append(pad_in_app + "/.gitkeep")
|
||||
|
||||
u.check("%s: alle mounts met een pad staan onder data/" % app,
|
||||
not buiten, "erbuiten: %r" % buiten)
|
||||
u.check("%s: elke gemounte map bestaat in de repo" % app,
|
||||
not ontbreekt, "ontbreekt: %r" % ontbreekt)
|
||||
|
||||
|
||||
def test_containernamen_zijn_volledig(u, app):
|
||||
"""Verwijzingen tussen containers gebruiken de volledige naam.
|
||||
|
||||
Alle apps van umbrelOS delen één Docker-netwerk, dus een servicenaam als
|
||||
'agent' of 'server' is daar niet uniek. Heeft een tweede app dezelfde naam,
|
||||
dan verdeelt Docker de naam over beide containers en komt ongeveer de helft
|
||||
van het verkeer bij de verkeerde app uit.
|
||||
|
||||
Dat is geen theorie: op 28-08-2026 gaf de statuspagina van Evolu Relay
|
||||
afwisselend goede gegevens en een 404, omdat Electrum Gate óók een 'agent' op
|
||||
poort 8000 heeft. Van tien verzoeken kwamen er vijf verkeerd uit. Het maakte
|
||||
bovendien de pagina van Electrum Gate kapot, een app die tot dat moment werkte.
|
||||
|
||||
De vorm <app-id>_<service>_1 is wél uniek. Deze toets kijkt naar de plekken
|
||||
waar een naam als hostnaam wordt gebruikt: proxy_pass in een nginx-config, en
|
||||
de omgevingsvariabelen die een hostnaam dragen. Een nginx-config kan in de
|
||||
app-map staan (als *.template) of in het bouwrecept onder tools/ (als hij in
|
||||
een eigen image zit); beide worden gelezen.
|
||||
"""
|
||||
fout = []
|
||||
|
||||
bestanden = []
|
||||
for bestandsnaam in sorted(os.listdir(os.path.join(REPO, app))):
|
||||
if bestandsnaam.endswith(".template") or bestandsnaam == "docker-compose.yml":
|
||||
bestanden.append(os.path.join(REPO, app, bestandsnaam))
|
||||
recept = recept_map(app)
|
||||
if os.path.isdir(recept):
|
||||
for bestandsnaam in sorted(os.listdir(recept)):
|
||||
if bestandsnaam.endswith(".conf"):
|
||||
bestanden.append(os.path.join(recept, bestandsnaam))
|
||||
|
||||
for pad in bestanden:
|
||||
bestandsnaam = os.path.relpath(pad, REPO).replace(os.sep, "/")
|
||||
tekst = lees(pad)
|
||||
for regel in tekst.splitlines():
|
||||
kaal = regel.strip()
|
||||
if kaal.startswith("#"):
|
||||
continue
|
||||
|
||||
naam = None
|
||||
if kaal.startswith("proxy_pass http://"):
|
||||
naam = kaal[len("proxy_pass http://"):].split(":", 1)[0].split("/", 1)[0]
|
||||
elif "_HOST:" in kaal and not kaal.startswith("- "):
|
||||
waarde = kaal.split(":", 1)[1].strip().strip('"')
|
||||
# Een variabele van umbrelOS of een IP-adres is geen containernaam.
|
||||
if waarde and not waarde.startswith("${") and not waarde[0].isdigit():
|
||||
naam = waarde
|
||||
|
||||
if naam is None or naam in ("localhost", "127.0.0.1"):
|
||||
continue
|
||||
if not naam.startswith(app + "_"):
|
||||
fout.append("%s: %s" % (bestandsnaam, naam))
|
||||
|
||||
u.check("%s: verwijzingen naar containers gebruiken de volledige naam" % app,
|
||||
not fout, "korte namen: %r" % fout)
|
||||
|
||||
|
||||
def test_geen_werkbestanden_in_de_app_map(u, app):
|
||||
"""In een app-map staat alleen wat umbreld nodig heeft.
|
||||
|
||||
umbreld kopieert bij een installatie de héle app-map naar
|
||||
`~/umbrel/app-data/<app-id>/` met `rsync --archive`. Alles wat daar staat
|
||||
belandt dus op het apparaat en in de back-up: een CLAUDE.md, een gelaagd
|
||||
bewerkbestand van een icoon, een testscript, een aantekening.
|
||||
|
||||
Dat is geen theorie. Op 30-08-2026 stond er een `icon.pdn` van 175 kB in de map
|
||||
van Evolu Relay, een Paint.NET-bestand van een icoon dat nog niet af was. Het
|
||||
apparaat kan er niets mee en het maakt elke back-up groter. Verplaatst naar
|
||||
`tools/icons/`; deze toets is wat voorkomt dat de volgende terugkomt.
|
||||
|
||||
De lijst hieronder is een whitelist en geen blacklist, en dat is met opzet: bij
|
||||
een nieuw soort bestand hoort iemand na te denken of het daar hoort, en een
|
||||
blacklist stelt die vraag nooit.
|
||||
"""
|
||||
TOEGESTAAN_EXACT = {
|
||||
"umbrel-app.yml", "docker-compose.yml", "exports.sh", "torrc",
|
||||
"icon.png", "icon.svg", ".gitkeep",
|
||||
}
|
||||
TOEGESTAANE_MAPPEN = {"data", "hooks"}
|
||||
|
||||
fout = []
|
||||
for naam in sorted(os.listdir(os.path.join(REPO, app))):
|
||||
pad = os.path.join(REPO, app, naam)
|
||||
|
||||
if os.path.isdir(pad):
|
||||
# __pycache__ is niet gecommit (het staat in .gitignore) en komt dus
|
||||
# nooit in de kloon die umbreld ophaalt. Het staat er in een werkboom
|
||||
# zodra een test de agent-template importeert, en daarover klagen zou
|
||||
# de toets rood zetten op iets dat het apparaat niet bereikt.
|
||||
if naam == "__pycache__":
|
||||
continue
|
||||
if naam not in TOEGESTAANE_MAPPEN:
|
||||
fout.append(naam + "/")
|
||||
continue
|
||||
|
||||
if naam in TOEGESTAAN_EXACT:
|
||||
continue
|
||||
# Alles wat umbreld bij een update ververst is een template, en die horen
|
||||
# er dus per definitie.
|
||||
if naam.endswith(".template"):
|
||||
continue
|
||||
fout.append(naam)
|
||||
|
||||
u.check("%s: er staan geen werkbestanden in de app-map" % app,
|
||||
not fout,
|
||||
"deze horen buiten de app-map: %r" % fout)
|
||||
|
||||
|
||||
# De variabelen die umbrelOS werkelijk invult. Alleen deze mogen in een template
|
||||
# staan; zie test_templates_hebben_geen_losse_dollars. Overgenomen uit de tabel in
|
||||
# HomeGit/Docs/Referenties/UmbrelApps/Umbrel-appstore-spec.md §3.
|
||||
UMBREL_VARIABELEN = {
|
||||
"APP_ID", "APP_VERSION", "APP_DATA_DIR", "APP_MANIFEST_FILE", "UMBREL_ROOT",
|
||||
"DEVICE_HOSTNAME", "DEVICE_DOMAIN_NAME", "APP_DOMAIN",
|
||||
"APP_PROXY_HOSTNAME", "APP_PROXY_PORT", "NETWORK_IP",
|
||||
"TOR_PROXY_IP", "TOR_PROXY_PORT", "TOR_DATA_DIR",
|
||||
"APP_HIDDEN_SERVICE", "APP_SEED", "APP_PASSWORD",
|
||||
}
|
||||
|
||||
# Een app krijgt daarnaast de exports van zijn afhankelijkheden, en die heten
|
||||
# APP_<AFHANKELIJKHEID>_<IETS>. Electrum Gate gebruikt APP_ELECTRS_NODE_IP en
|
||||
# APP_ELECTRS_NODE_PORT. Die kunnen niet in een vaste lijst staan, want welke er
|
||||
# zijn hangt af van de afhankelijkheid; vandaar een vorm in plaats van een naam.
|
||||
EXPORT_VORM = r"^APP_[A-Z0-9]+_[A-Z0-9_]+$"
|
||||
|
||||
|
||||
def test_templates_hebben_geen_losse_dollars(u, app):
|
||||
"""In een *.template staat geen dollarteken dat umbrelOS niet invult.
|
||||
|
||||
Dit is de valstrik van dit hele project, en tot 30-08-2026 beweerde het
|
||||
commentaar in drie bestanden dat déze test hem dichthield terwijl dat niet zo
|
||||
was.
|
||||
|
||||
Wat er gebeurt: umbreld haalt elke `*.template` bij het starten door envsubst.
|
||||
Dat vervangt élke accolade-vorm, ook een variabele die niet bestaat, en die
|
||||
wordt dan leeg. Gevolgen per bestandstype:
|
||||
|
||||
- in een pagina sloopt het een JavaScript-template-literal, want die gebruikt
|
||||
accolades achter een dollarteken. Vandaar dat de pagina's overal strings met
|
||||
een plus aan elkaar plakken;
|
||||
- in een nginx-config verdwijnen `host`, `log_format` en elke variabele in een
|
||||
access_log;
|
||||
- in Python-code verdwijnt stilzwijgend een stuk code.
|
||||
|
||||
Er komt geen foutmelding. Het bestand wordt gewoon anders dan je schreef.
|
||||
|
||||
Beide vormen die envsubst kent worden gevlagd: `${NAAM}` en het kale `$NAAM`.
|
||||
Dat tweede is de vorm waarin een nginx-variabele als `$host` geschreven wordt,
|
||||
en die is hier dus net zo fout als de eerste.
|
||||
|
||||
Wat NIET gevlagd wordt is een los dollarteken zonder naam erachter. Dat laat
|
||||
envsubst staan, en het staat in dit project in de commentaarregels die deze
|
||||
regel juist uitleggen: een toets die zijn eigen uitleg rood zet, wordt
|
||||
uitgezet.
|
||||
"""
|
||||
import re
|
||||
|
||||
for bestandsnaam in sorted(os.listdir(os.path.join(REPO, app))):
|
||||
if not bestandsnaam.endswith(".template"):
|
||||
continue
|
||||
|
||||
tekst = lees(os.path.join(REPO, app, bestandsnaam))
|
||||
onbekend = []
|
||||
for regelnummer, regel in enumerate(tekst.splitlines(), start=1):
|
||||
for treffer in re.finditer(r"\$\{?([A-Za-z_][A-Za-z0-9_]*)\}?", regel):
|
||||
naam = treffer.group(1)
|
||||
if naam in UMBREL_VARIABELEN:
|
||||
continue
|
||||
if re.match(EXPORT_VORM, naam):
|
||||
continue
|
||||
onbekend.append("regel %d: %r" % (regelnummer, treffer.group(0)))
|
||||
|
||||
u.check("%s/%s: geen dollartekens die umbrelOS leegmaakt" % (app, bestandsnaam),
|
||||
not onbekend,
|
||||
"gevonden: %r" % onbekend[:4])
|
||||
|
||||
|
||||
def rapporteer_pinstatus(apps):
|
||||
"""Afdrukken, niet toetsen. Zie de uitleg bovenaan dit bestand."""
|
||||
print()
|
||||
print("Pinstatus van de images (informatief, geen toets):")
|
||||
for app in apps:
|
||||
pad = os.path.join(REPO, app, "docker-compose.yml")
|
||||
if not os.path.isfile(pad):
|
||||
continue
|
||||
for regel in lees(pad).splitlines():
|
||||
kaal = regel.strip()
|
||||
if not kaal.startswith("image:"):
|
||||
continue
|
||||
image = kaal.split(":", 1)[1].strip()
|
||||
merk = "gepind" if "@sha256:" in image else "NIET GEPIND"
|
||||
print(" %-24s %-12s %s" % (app, merk, image))
|
||||
|
||||
|
||||
def main():
|
||||
u = Uitslag()
|
||||
store_id = veld(lees(STORE), "id")
|
||||
apps = app_mappen()
|
||||
|
||||
test_er_is_een_store_met_apps(u, store_id, apps)
|
||||
for app in apps:
|
||||
test_id_en_mapnaam(u, store_id, app)
|
||||
test_verplichte_velden(u, app)
|
||||
test_manifest_volgorde(u, app)
|
||||
test_compose_bestaat_en_hangt_samen(u, app)
|
||||
test_image_tag_is_de_gebouwde(u, app)
|
||||
test_data_onder_data(u, app)
|
||||
test_containernamen_zijn_volledig(u, app)
|
||||
test_geen_werkbestanden_in_de_app_map(u, app)
|
||||
test_templates_hebben_geen_losse_dollars(u, app)
|
||||
|
||||
rapporteer_pinstatus(apps)
|
||||
return u.rapport()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in new issue
Block a user