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)
|
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):
|
def test_containernamen_zijn_volledig(u, app):
|
||||||
"""Verwijzingen tussen containers gebruiken de volledige naam.
|
"""Verwijzingen tussen containers gebruiken de volledige naam.
|
||||||
|
|
||||||
@@ -596,6 +736,7 @@ def main():
|
|||||||
test_image_tag_is_de_gebouwde(u, app)
|
test_image_tag_is_de_gebouwde(u, app)
|
||||||
test_data_onder_data(u, app)
|
test_data_onder_data(u, app)
|
||||||
test_verplaatsbare_opslag(u, app)
|
test_verplaatsbare_opslag(u, app)
|
||||||
|
test_folder_access(u, app)
|
||||||
test_containernamen_zijn_volledig(u, app)
|
test_containernamen_zijn_volledig(u, app)
|
||||||
test_geen_werkbestanden_in_de_app_map(u, app)
|
test_geen_werkbestanden_in_de_app_map(u, app)
|
||||||
test_templates_hebben_geen_losse_dollars(u, app)
|
test_templates_hebben_geen_losse_dollars(u, app)
|
||||||
|
|||||||
Whitespace-only changes.
@@ -34,7 +34,7 @@ services:
|
|||||||
# DE TAG IS DE VERSIE VAN DE IMAGE EN NIET DIE VAN DE APP, net als bij Atreides
|
# DE TAG IS DE VERSIE VAN DE IMAGE EN NIET DIE VAN DE APP, net als bij Atreides
|
||||||
# Assets: `version` in umbrel-app.yml moet bij élke wijziging omhoog, ook als
|
# Assets: `version` in umbrel-app.yml moet bij élke wijziging omhoog, ook als
|
||||||
# alleen dit bestand verandert.
|
# alleen dit bestand verandert.
|
||||||
image: sc.kamenier-hamer.nl/sysop/atreides-services:0.0.1@sha256:57e509700bed543048544eb6d1fece9386c2e0a715dfb901e381d4f9247b2815
|
image: sc.kamenier-hamer.nl/sysop/atreides-services:0.0.2@sha256:f46330027f8289ba263dcbca15bda5ea00de8c2f64bcb17b71974ab7497c60ad
|
||||||
restart: on-failure
|
restart: on-failure
|
||||||
# Geen `user:` nodig: de image zet zelf `USER node` (uid 1000). umbrelOS
|
# Geen `user:` nodig: de image zet zelf `USER node` (uid 1000). umbrelOS
|
||||||
# maakt de datamap aan als `umbrel`, ook uid 1000, dus node mag erin
|
# maakt de datamap aan als `umbrel`, ook uid 1000, dus node mag erin
|
||||||
@@ -64,3 +64,10 @@ services:
|
|||||||
# (.gitkeep), en mag hier geen mount-optie bij die umbreld niet kent.
|
# (.gitkeep), en mag hier geen mount-optie bij die umbreld niet kent.
|
||||||
# Staat de map op een USB-schijf, dan gaat hij NIET mee in de back-up.
|
# Staat de map op een USB-schijf, dan gaat hij NIET mee in de back-up.
|
||||||
- ${APP_DATA_DIR}/data/app:/data
|
- ${APP_DATA_DIR}/data/app:/data
|
||||||
|
# De standaard van het folderAccess-slot "backups" (umbrel-app.yml): zelfde
|
||||||
|
# service en zelfde doelpad, en precies de map die de app zonder deze regel
|
||||||
|
# ook zou gebruiken. Wijst de gebruiker het slot naar een NAS-map, dan
|
||||||
|
# vervangt umbreld alleen deze mount; de rest van /data blijft lokaal.
|
||||||
|
# Gemeten in het plan Netwerkopslag: een mount binnen /data verbergt de rest
|
||||||
|
# niet.
|
||||||
|
- ${APP_DATA_DIR}/data/app/backups:/data/backups
|
||||||
@@ -14,9 +14,27 @@ id: whatsnext-atreides-services
|
|||||||
# start dan niet meer. Zie Umbrel-appstore-spec.md §7.
|
# start dan niet meer. Zie Umbrel-appstore-spec.md §7.
|
||||||
storage:
|
storage:
|
||||||
dataRoot: data
|
dataRoot: data
|
||||||
|
# umbrelOS 2.0: een map die de gebruiker in de app-instellingen naar zijn
|
||||||
|
# Home-map, een USB-schijf of een netwerkshare kan wijzen. Er wordt niets
|
||||||
|
# gekopieerd; umbreld vervangt alleen de mount. De standaard is de mount op
|
||||||
|
# dezelfde service en hetzelfde doelpad in docker-compose.yml, en die wijst naar
|
||||||
|
# dezelfde plek als zonder dit slot, dus voor een bestaande installatie
|
||||||
|
# verandert er niets. De app schrijft in een eigen submap backups/ met een
|
||||||
|
# merkbestand, zodat dit slot en het latere "Price history" naar dezelfde
|
||||||
|
# NAS-map mogen wijzen.
|
||||||
|
#
|
||||||
|
# Het `id` is na de eerste release vast: umbrelOS bewaart de keuze van de
|
||||||
|
# gebruiker per id, dus hernoemen gooit die weg. Op een netwerkshare valt de map
|
||||||
|
# buiten de back-up van umbrelOS, en dat is hier juist de bedoeling.
|
||||||
|
folderAccess:
|
||||||
|
- id: backups
|
||||||
|
name: Backups
|
||||||
|
mounts:
|
||||||
|
- service: server
|
||||||
|
targetPath: /data/backups
|
||||||
category: finance
|
category: finance
|
||||||
name: Atreides Services
|
name: Atreides Services
|
||||||
version: "0.0.3"
|
version: "0.0.4"
|
||||||
tagline: Your own price feed, recorded around the clock
|
tagline: Your own price feed, recorded around the clock
|
||||||
description: >-
|
description: >-
|
||||||
A price service that runs on your Umbrel and keeps what it records. It fetches prices day
|
A price service that runs on your Umbrel and keeps what it records. It fetches prices day
|
||||||
@@ -34,9 +52,10 @@ description: >-
|
|||||||
mempool.space, and brings the administration app with its System page and an optional
|
mempool.space, and brings the administration app with its System page and an optional
|
||||||
login of its own. The API for other apps follows in a later version.
|
login of its own. The API for other apps follows in a later version.
|
||||||
releaseNotes: >-
|
releaseNotes: >-
|
||||||
On umbrelOS 2.0 the app's storage can now be moved to a USB drive (ext4) in the app
|
The app now makes a checked backup every night, and keeps each closed month once. On
|
||||||
settings. Keep in mind that umbrelOS does not back up an app whose storage is on a USB
|
umbrelOS 2.0 you can point the new Backups folder at a folder on your NAS in the app
|
||||||
drive.
|
settings, so the backups live on another disk. Without that, they stay in the app's own
|
||||||
|
storage. The app writes them in a subfolder backups/ of the folder you pick.
|
||||||
developer: "WhatsNext?"
|
developer: "WhatsNext?"
|
||||||
website: https://sc.kamenier-hamer.nl/sysop/UmbrelApps
|
website: https://sc.kamenier-hamer.nl/sysop/UmbrelApps
|
||||||
dependencies: []
|
dependencies: []
|
||||||
|
|||||||
Reference in new issue
Block a user