Atreides Services 0.0.4: image 0.0.2 en het slot Backups
- image 0.0.2 met digest, via tools/release-bump.mjs; de digest komt
overeen met wat het register anoniem onder die tag heeft.
- folderAccess-slot backups (Backups) op server:/data/backups, met als
standaard ${APP_DATA_DIR}/data/app/backups en een .gitkeep. Zonder
gekozen map verandert er niets: dezelfde plek als zonder het slot.
- test_appstore_vorm.py toetst elk folderAccess-slot: geldig en uniek id,
een naam, een service die bestaat en een standaardmount op hetzelfde
doelpad. Mutatie op id, doelpad en service valt elk op de juiste toets.
Het slot Price history volgt met fase 2c van het plan Netwerkopslag.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
e86208ddfe
commit
519002902c
4 files changed
+172
-5
No files matched your search
@@ -388,6 +388,146 @@ def test_verplaatsbare_opslag(u, app):
|
||||
not fout, "geweigerd: %r" % fout)
|
||||
|
||||
|
||||
def folder_slots(tekst):
|
||||
"""De slots onder `folderAccess:` als lijst van dicts, elk met een lijst `mounts`.
|
||||
|
||||
Geen YAML-lezer, zie veld(). Wat telt is de inspringing: een streepje op de
|
||||
diepte van het eerste streepje begint een slot, een dieper streepje een mount
|
||||
in dat slot. Een sleutel zonder streepje hoort bij de mount zolang hij dieper
|
||||
staat dan de sleutels van het slot.
|
||||
"""
|
||||
regels = tekst.splitlines()
|
||||
start = next((i for i, r in enumerate(regels) if r.startswith("folderAccess:")), None)
|
||||
if start is None:
|
||||
return None
|
||||
|
||||
slots = []
|
||||
slot_streep = None
|
||||
huidig = doel = None
|
||||
for r in regels[start + 1:]:
|
||||
if r and not r[0].isspace():
|
||||
break
|
||||
kaal = r.strip()
|
||||
if not kaal or kaal.startswith("#"):
|
||||
continue
|
||||
inspring = len(r) - len(r.lstrip())
|
||||
if kaal.startswith("- "):
|
||||
kaal = kaal[2:].strip()
|
||||
if slot_streep is None or inspring <= slot_streep:
|
||||
slot_streep = inspring
|
||||
huidig = doel = {"mounts": []}
|
||||
slots.append(huidig)
|
||||
elif huidig is not None:
|
||||
doel = {}
|
||||
huidig["mounts"].append(doel)
|
||||
elif huidig is not None and doel is not huidig and inspring <= slot_streep + 2:
|
||||
doel = huidig
|
||||
if doel is None or ":" not in kaal:
|
||||
continue
|
||||
sleutel, waarde = kaal.split(":", 1)
|
||||
waarde = waarde.strip().strip('"').strip("'")
|
||||
if waarde:
|
||||
doel[sleutel.strip()] = waarde
|
||||
return slots
|
||||
|
||||
|
||||
def doelpaden_per_service(tekst):
|
||||
"""Per service in de compose de doelpaden van de korte `volumes:`-regels."""
|
||||
uit = {}
|
||||
in_services = False
|
||||
service = None
|
||||
in_volumes = False
|
||||
volumes_inspring = 0
|
||||
for regel in tekst.splitlines():
|
||||
if regel.startswith("services:"):
|
||||
in_services = True
|
||||
continue
|
||||
if not in_services:
|
||||
continue
|
||||
if regel and not regel[0].isspace():
|
||||
break
|
||||
kaal = regel.strip()
|
||||
if not kaal or kaal.startswith("#"):
|
||||
continue
|
||||
inspring = len(regel) - len(regel.lstrip())
|
||||
if inspring == 2 and kaal.endswith(":"):
|
||||
service = kaal[:-1]
|
||||
uit[service] = []
|
||||
in_volumes = False
|
||||
continue
|
||||
if service is None:
|
||||
continue
|
||||
if kaal == "volumes:":
|
||||
in_volumes = True
|
||||
volumes_inspring = inspring
|
||||
continue
|
||||
if in_volumes:
|
||||
if kaal.startswith("- "):
|
||||
delen = kaal[2:].strip().strip('"').split(":")
|
||||
if len(delen) >= 2:
|
||||
uit[service].append(delen[1])
|
||||
elif inspring <= volumes_inspring:
|
||||
in_volumes = False
|
||||
return uit
|
||||
|
||||
|
||||
def test_folder_access(u, app):
|
||||
"""Elk `folderAccess`-slot wijst naar een service die bestaat, met een standaardmount.
|
||||
|
||||
umbrelOS 2.0 bewaart de keuze van de gebruiker per `id`, en zoekt de standaard
|
||||
van een slot in de compose: een mount op dezelfde `service` en hetzelfde
|
||||
`targetPath`. Klopt een van de twee niet, dan meldt niemand iets: het slot is
|
||||
dan "Optional" met een lege standaard, en tot de gebruiker een map kiest
|
||||
schrijft de app in wat er toevallig onder dat pad ligt, in het slechtste geval
|
||||
zijn eigen containerlaag, die bij de volgende update weg is. In deze store heeft elk slot
|
||||
daarom een standaardmount onder data/; dat die map in de repo staat, toetst
|
||||
test_data_onder_data.
|
||||
|
||||
`umbrel-downloads` is gereserveerd voor de gedeelde Downloads-map. Zie
|
||||
Umbrel-appstore-spec.md §7.
|
||||
"""
|
||||
slots = folder_slots(lees(os.path.join(REPO, app, "umbrel-app.yml")))
|
||||
if slots is None:
|
||||
return
|
||||
|
||||
u.check("%s: folderAccess heeft minstens een slot" % app, bool(slots))
|
||||
|
||||
ids = [s.get("id") for s in slots]
|
||||
fout_id = [i for i in ids if not i or i != i.lower() or " " in i or "_" in i
|
||||
or i == "umbrel-downloads"]
|
||||
u.check("%s: elk slot-id is lowercase kebab-case en niet gereserveerd" % app,
|
||||
not fout_id, "ids: %r" % ids)
|
||||
u.check("%s: de slot-ids zijn uniek" % app,
|
||||
len(set(ids)) == len(ids), "ids: %r" % ids)
|
||||
zonder_naam = [s.get("id") for s in slots if not s.get("name")]
|
||||
u.check("%s: elk slot heeft een naam" % app,
|
||||
not zonder_naam, "zonder naam: %r" % zonder_naam)
|
||||
|
||||
pad = os.path.join(REPO, app, "docker-compose.yml")
|
||||
if not os.path.isfile(pad):
|
||||
return
|
||||
doelen = doelpaden_per_service(lees(pad))
|
||||
|
||||
zonder_mount = [s.get("id") for s in slots if not s["mounts"]]
|
||||
u.check("%s: elk slot heeft minstens een mount" % app,
|
||||
not zonder_mount, "zonder mount: %r" % zonder_mount)
|
||||
|
||||
onbekend = []
|
||||
zonder_standaard = []
|
||||
for s in slots:
|
||||
for m in s["mounts"]:
|
||||
service = m.get("service")
|
||||
doel = m.get("targetPath", "")
|
||||
if service not in doelen:
|
||||
onbekend.append("%s: %r" % (s.get("id"), service))
|
||||
elif doel not in doelen[service]:
|
||||
zonder_standaard.append("%s: %s op %s" % (s.get("id"), doel, service))
|
||||
u.check("%s: elke slot-mount wijst naar een service die bestaat" % app,
|
||||
not onbekend, "onbekend: %r, aanwezig: %r" % (onbekend, sorted(doelen)))
|
||||
u.check("%s: elke slot-mount heeft een standaardmount in de compose" % app,
|
||||
not zonder_standaard, "ontbreekt: %r" % zonder_standaard)
|
||||
|
||||
|
||||
def test_containernamen_zijn_volledig(u, app):
|
||||
"""Verwijzingen tussen containers gebruiken de volledige naam.
|
||||
|
||||
@@ -596,6 +736,7 @@ def main():
|
||||
test_image_tag_is_de_gebouwde(u, app)
|
||||
test_data_onder_data(u, app)
|
||||
test_verplaatsbare_opslag(u, app)
|
||||
test_folder_access(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)
|
||||
|
||||
Reference in new issue
Block a user