Gate 0.1.0: eigen image, het command-blok is een script, de app-map is leeg

Plan Eigenimage, fase 1 tot en met 3. De vier templates verhuizen naar
tools/electrum-gate/ zonder extensie; daarnaast Dockerfile (nginx:1.30-alpine
plus python3), entrypoint.sh (het command-blok van de compose, zonder $$) en
build.sh naar het voorbeeld van Evolu Relay. Een image voor beide containers,
gebouwd op de Umbrel; open punt 2 en 3 daarmee beslist.

Inhoudelijk anders dan alleen verplaatst: het log_format staat in stream.conf
zelf, het backend-adres komt via twee plaatshouders zonder dollarteken uit de
omgeving (ook in de server-service), en de pagina haalt versie en adres uit
status.json via GATE_APP_VERSION.

Tests mee verhuisd en uitgebreid: entrypoint.sh en Dockerfile in plaats van het
command-blok, en de tag in de compose gelijk aan VERSION in build.sh voor elke
eigen image. Mutatie-getest met drie ingrepen.

Nog niet gebouwd: er is hier geen Docker. De tag staat ongepind tot de eerste
push; dat is fase 4 en die is van de gebruiker.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Harmen
2026-09-07 20:20:42 +02:00
co-authored by Claude Fable 5.1
parent 7b3804df89
commit 973b24a23f
25 changed files with 981 additions and 410 deletions
+22 -18
View File
@@ -11,16 +11,18 @@ Draaien:
python tests/test_agent_certificates.py
De test laadt `agent.py.template` rechtstreeks. Dat kan omdat dat bestand geen
accolade-variabelen bevat en de invulling door umbreld hem dus onveranderd laat;
de eerste toets hieronder controleert precies dat.
De test laadt `tools/electrum-gate/agent.py` rechtstreeks: het bestand dat de
Dockerfile in de image zet. Tot 0.1.0 heette dat `agent.py.template` en stond het
in de app-map; de eerste toets hieronder is uit die tijd en blijft staan, want
het script haalt zijn instellingen nog altijd uit de omgeving en niet uit een
ingevulde constante.
"""
import sys
# Vóór de imports, want anders is het te laat: Python legt bytecode naast
# agent.py.template zodra die geïmporteerd wordt, en die rommel hoort niet in de
# app-map. Een keer is zo'n .pyc meegegaan in een commit.
# Vóór de imports, want anders is het te laat: Python legt bytecode naast agent.py
# zodra die geïmporteerd wordt, en die rommel hoort niet in de repo. Een keer is
# zo'n .pyc meegegaan in een commit.
sys.dont_write_bytecode = True
import importlib.machinery # noqa: E402
@@ -33,17 +35,13 @@ import subprocess # noqa: E402
import tempfile # noqa: E402
HERE = os.path.dirname(os.path.abspath(__file__))
TEMPLATE = os.path.join(HERE, os.pardir, "whatsnext-electrum-gate", "agent.py.template")
AGENT = os.path.join(HERE, os.pardir, "tools", "electrum-gate", "agent.py")
def load_agent():
"""Laadt agent.py.template als module.
Met een expliciete loader, want importlib kijkt normaal naar de extensie en
.template staat daar niet tussen.
"""
loader = importlib.machinery.SourceFileLoader("gate_agent", TEMPLATE)
spec = importlib.util.spec_from_file_location("gate_agent", TEMPLATE, loader=loader)
"""Laadt agent.py als module, buiten sys.path om."""
loader = importlib.machinery.SourceFileLoader("gate_agent", AGENT)
spec = importlib.util.spec_from_file_location("gate_agent", AGENT, loader=loader)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module
@@ -83,12 +81,18 @@ def nep_certificaat(tijdtag, tijdtekst):
def test_template_is_invulbaar_zonder_schade(u):
"""De aanname waar deze hele test op rust."""
with open(TEMPLATE, "r", encoding="utf-8") as f:
"""Een restant uit de template-tijd dat blijft staan.
Het bestand zit sinds 0.1.0 in de image en gaat door geen envsubst meer. Maar
een accolade-variabele in Python-code is ook zonder umbreld een fout, en als
iemand dit bestand ooit weer als template in een app-map zet, is dit de toets
die het meteen ziet.
"""
with open(AGENT, "r", encoding="utf-8") as f:
inhoud = f.read()
u.check("template bevat geen accolade-variabelen",
u.check("agent.py bevat geen accolade-variabelen",
"${" not in inhoud,
"umbreld zou die invullen en de Python-code slopen")
"een envsubst-stap zou die invullen en de Python-code slopen")
def test_einddatum_gelijk_aan_ssl(agent, u):
+74 -5
View File
@@ -20,7 +20,9 @@ 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.
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.
@@ -172,6 +174,61 @@ def test_manifest_volgorde(u, 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 CLAUDE.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 CLAUDE.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.
@@ -285,14 +342,25 @@ def test_containernamen_zijn_volledig(u, app):
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.
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 not bestandsnaam.endswith(".template") and bestandsnaam != "docker-compose.yml":
continue
tekst = lees(os.path.join(REPO, app, bestandsnaam))
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("#"):
@@ -464,6 +532,7 @@ def main():
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)
+35 -5
View File
@@ -99,14 +99,35 @@ const parseert = (bron) => {
}
};
/**
* Waar de pagina van een app staat, en of umbreld hem invult.
*
* Twee plekken: als *.template in de app-map (dan gaat hij door envsubst), of als
* gewoon bestand in het bouwrecept onder tools/<app-id zonder store-id>/ (dan zit
* hij in een eigen image en wordt hij door niemand ingevuld). Electrum Gate is
* sinds 0.1.0 het tweede geval; zie het plan Eigenimage.
*/
const vindPagina = (app) => {
const template = join(REPO, app, 'index.html.template');
if (existsSync(template)) {
return { pad: template, template: true };
}
const kort = app.includes('-') ? app.slice(app.indexOf('-') + 1) : app;
const inImage = join(REPO, 'tools', kort, 'index.html');
if (existsSync(inImage)) {
return { pad: inImage, template: false };
}
return null;
};
for (const app of appMappen()) {
const pad = join(REPO, app, 'index.html.template');
if (!existsSync(pad)) {
const pagina = vindPagina(app);
if (pagina === null) {
// Geen pagina is geen fout: een app hoeft er geen te hebben.
continue;
}
const bron = readFileSync(pad, 'utf8');
const bron = readFileSync(pagina.pad, 'utf8');
const blokken = scriptBlokken(bron);
toets(`${app}: de pagina heeft een scriptblok`, blokken.length > 0);
@@ -117,10 +138,19 @@ for (const app of appMappen()) {
const rauw = parseert(blok);
toets(`${naam} parseert zoals hij in de repo staat`, rauw === null, rauw);
const ingevuld = parseert(vulIn(blok));
toets(`${naam} parseert ook nadat umbreld hem invult`, ingevuld === null, ingevuld);
if (pagina.template) {
const ingevuld = parseert(vulIn(blok));
toets(`${naam} parseert ook nadat umbreld hem invult`, ingevuld === null, ingevuld);
}
});
if (!pagina.template) {
// Een pagina in een image gaat door geen envsubst meer, dus wat er staat is
// wat de browser krijgt. Een accolade-variabele is dan een restant uit de
// template-tijd die letterlijk op de pagina zou belanden.
toets(`${app}: de pagina in de image bevat geen accolade-variabele`, !bron.includes('${'));
}
// De HTML zelf: geen parser, maar wel de fout die je in een bestand van
// tweeduizend regels echt maakt. Een dialog of een header die niet gesloten is,
// laat de rest van de pagina in dat element hangen.
+211 -62
View File
@@ -9,18 +9,25 @@ niets. De symptomen wezen ergens anders heen: de app_proxy meldde alleen dat de
server niet te bereiken was.
De reparatie is een verplaatsing, en die is niet aan de code af te lezen: het
stream-blok staat nu in stream.conf.template en wordt door het command-blok van
de compose pas in /var/lib/gate/tls/ gezet als cert.conf bestaat. nginx.conf
haalt die map op met een jokerteken, en een jokerteken dat niets matcht is voor
nginx geen fout. Eén iemand die dat blok "netjes" terugzet in nginx.conf en de
klem is terug, zonder dat een van de andere toetsen iets merkt.
stream-blok staat in stream.conf en wordt door entrypoint.sh pas in
/var/lib/gate/tls/ gezet als cert.conf bestaat. nginx.conf haalt die map op met
een jokerteken, en een jokerteken dat niets matcht is voor nginx geen fout. Eén
iemand die dat blok "netjes" terugzet in nginx.conf en de klem is terug, zonder
dat een van de andere toetsen iets merkt.
Sinds 0.1.0 zitten deze bestanden in een eigen image en staan ze in
tools/electrum-gate/; tot dan waren het *.template-bestanden in de app-map en
was het startscript een command-blok in de compose. De toetsen zijn mee
verhuisd en toetsen hetzelfde gedrag, plus wat er bij die verhuizing nieuw
kapot kan: de Dockerfile die een bestand niet meeneemt, en de plaatshouders in
stream.conf die entrypoint.sh moet invullen.
Draaien:
python tests/test_server_start_zonder_certificaat.py
Er staan inmiddels een paar toetsen bij die niet over het starten gaan maar wel
over dezelfde soort fout: een die niets meldt en pas opvalt als iemand het toevallig
Er staan een paar toetsen bij die niet over het starten gaan maar wel over
dezelfde soort fout: een die niets meldt en pas opvalt als iemand het toevallig
ziet. De verbindingsteller die stil nul telt, een mount die weer alleen-lezen wordt,
en twee taglines die uit elkaar lopen. Ze staan hier en niet in een derde bestand,
want de suite heeft geen runner en een bestand dat niemand aanroept toetst niets.
@@ -31,15 +38,20 @@ import sys
sys.dont_write_bytecode = True
import os # noqa: E402
import re # noqa: E402
HERE = os.path.dirname(os.path.abspath(__file__))
APP = os.path.join(HERE, os.pardir, "whatsnext-electrum-gate")
RECEPT = os.path.join(HERE, os.pardir, "tools", "electrum-gate")
NGINX_CONF = os.path.join(APP, "nginx.conf.template")
STREAM_CONF = os.path.join(APP, "stream.conf.template")
NGINX_CONF = os.path.join(RECEPT, "nginx.conf")
STREAM_CONF = os.path.join(RECEPT, "stream.conf")
ENTRYPOINT = os.path.join(RECEPT, "entrypoint.sh")
DOCKERFILE = os.path.join(RECEPT, "Dockerfile")
BUILD = os.path.join(RECEPT, "build.sh")
PAGINA = os.path.join(RECEPT, "index.html")
COMPOSE = os.path.join(APP, "docker-compose.yml")
MANIFEST = os.path.join(APP, "umbrel-app.yml")
PAGINA = os.path.join(APP, "index.html.template")
class Uitslag:
@@ -67,7 +79,7 @@ def lees(pad):
def zonder_commentaar(tekst):
"""De regels die nginx daadwerkelijk uitvoert.
"""De regels die nginx of de shell daadwerkelijk uitvoert.
Zonder dit zou elke toets hieronder afgaan op de uitleg erboven, en die
noemt juist de dingen die er niet meer mogen staan.
@@ -104,64 +116,71 @@ def test_pagina_hangt_niet_aan_een_certificaat(u):
def test_stream_blok_staat_apart(u):
"""Het TLS-deel is compleet, en het is een template."""
u.check("stream.conf.template bestaat", os.path.isfile(STREAM_CONF),
"anders komt het bij een update niet mee: alleen *.template, "
"docker-compose.yml, exports.sh, torrc, hooks en umbrel-app.yml "
"worden ververst")
"""Het TLS-deel is compleet, en het backend-adres komt uit de omgeving."""
u.check("stream.conf bestaat", os.path.isfile(STREAM_CONF))
if not os.path.isfile(STREAM_CONF):
return
tekst = lees(STREAM_CONF)
regels = zonder_commentaar(tekst)
regels = zonder_commentaar(lees(STREAM_CONF))
u.check("stream.conf luistert op 50022 met ssl",
any("listen 50022 ssl" in r for r in regels))
u.check("stream.conf includet de cert.conf van de agent",
any("include /var/lib/gate/cert.conf" in r for r in regels))
u.check("stream.conf includet het log_format",
any("include /var/lib/gate/stream-log.conf" in r for r in regels))
u.check("stream.conf verwijst naar de backend uit de omgeving",
any("proxy_pass ${APP_ELECTRS_NODE_IP}" in r for r in regels))
u.check("stream.conf heeft zijn log_format zelf",
any(r.startswith("log_format gate") for r in regels),
"tot 0.0.29 schreef het command-blok dat in een los bestand, omdat "
"een dollarteken in een template leeggemaakt werd; dat hoeft niet meer")
u.check("en schrijft de sessielog waar de agent hem leest",
any(r.startswith("access_log /var/lib/gate/stream.log") for r in regels))
# Dezelfde architectuurregel als voor de andere templates: umbreld vult elke
# accolade-variabele in, dus een nginx-variabele met een dollarteken wordt
# hier stil leeggemaakt. Vandaar dat het log_format uit een ander bestand
# komt.
losse_dollars = []
for regel in regels:
pos = regel.find("$")
while pos >= 0:
if regel[pos + 1:pos + 2] != "{":
losse_dollars.append(regel)
break
pos = regel.find("$", pos + 1)
u.check("stream.conf heeft geen nginx-variabelen",
not losse_dollars,
"de template-invulling maakt die leeg: %r" % losse_dollars[:2])
# Het adres van de backend staat niet in dit bestand maar komt uit de
# omgeving, en de weg daarheen is een plaatshouder die entrypoint.sh invult.
# Geen dollarteken in die plaatshouder: dan kan geen envsubst-stap, van
# umbreld of van wie dan ook, hem stil leegmaken.
proxy = [r for r in regels if r.startswith("proxy_pass")]
u.check("stream.conf heeft precies één proxy_pass", len(proxy) == 1,
"gevonden: %r" % proxy)
u.check("en die verwijst naar de plaatshouders voor host en poort",
proxy and "__GATE_ELECTRUM_HOST__:__GATE_ELECTRUM_PORT__" in proxy[0],
"gevonden: %r" % proxy)
u.check("stream.conf bevat geen accolade-variabele meer",
"${" not in lees(STREAM_CONF),
"die wordt sinds de eigen image door niemand meer ingevuld en zou "
"letterlijk in de configuratie belanden")
def test_compose_wacht_niet_op_de_agent(u):
"""Het command-blok start nginx altijd, en zet TLS erbij als het kan."""
tekst = lees(COMPOSE)
def test_entrypoint_wacht_niet_op_de_agent(u):
"""entrypoint.sh start nginx altijd, en zet TLS erbij als het kan."""
u.check("entrypoint.sh bestaat", os.path.isfile(ENTRYPOINT))
if not os.path.isfile(ENTRYPOINT):
return
tekst = lees(ENTRYPOINT)
regels = zonder_commentaar(tekst)
u.check("geen wachtlus op cert.conf",
not any("! -f /var/lib/gate/cert.conf" in r and "while" in r
not any("! -f" in r and "cert.conf" in r and "while" in r
for r in regels),
"dat is precies de klem uit 0.0.3: geen certificaat, geen nginx, "
"geen pagina, dus geen manier om een certificaat te kiezen")
u.check("stream.conf is in de server gemount",
any("stream.conf:/etc/nginx/stream.conf" in r for r in regels),
"anders is er niets om in /var/lib/gate/tls/ te zetten")
u.check("het stream-blok wordt ingevuld uit de omgeving",
any("__GATE_ELECTRUM_HOST__" in r and "GATE_ELECTRUM_HOST" in r
for r in regels)
and any("__GATE_ELECTRUM_PORT__" in r for r in regels),
"anders staat de plaatshouder letterlijk in de proxy_pass")
u.check("en wordt uit stream.conf.in gelezen, zoals de Dockerfile hem neerzet",
any("/etc/nginx/stream.conf.in" in r for r in regels))
u.check("het stream-blok wordt weggezet als cert.conf bestaat",
any("cp /etc/nginx/stream.conf /var/lib/gate/tls/" in r
any(r.startswith("if cp /etc/nginx/stream.conf") and "/tls/" in r
for r in regels))
u.check("en weggehaald als cert.conf er niet is",
any("rm -f /var/lib/gate/tls/stream.conf" in r for r in regels),
any(r.startswith("rm -f") and "/tls/stream.conf" in r for r in regels),
"een ingetrokken keuze moet de poort ook echt sluiten")
u.check("een mislukte herlading breekt de wachtlus niet af",
@@ -169,6 +188,119 @@ def test_compose_wacht_niet_op_de_agent(u):
"onder 'set -e' zou de lus verdwijnen en daarna pikt niets meer "
"een wijziging op")
u.check("nginx wordt met exec het hoofdproces",
regels and regels[-1].startswith("exec nginx"),
"anders blijft de shell PID 1, geeft signalen niet door, en ziet "
"Docker een geslaagde afsluiting als nginx omvalt; laatste regel: %r"
% (regels[-1] if regels else None))
u.check("het script begint met een shebang",
tekst.startswith("#!/bin/sh"))
u.check("geen verdubbelde dollartekens meer",
"$$" not in "\n".join(regels),
"dat was de ontsnapping voor het command-blok in de compose; in een "
"gewoon script is $$ het proces-id")
def test_dockerfile_neemt_alles_mee(u):
"""Wat entrypoint.sh en nginx.conf verwachten, zet de Dockerfile ook neer.
Dit is de nieuwe klasse fouten van de eigen image: een bestand dat in de
repo staat maar niet in de image, en dan faalt de container pas op het
apparaat. Geen Docker nodig om dat te zien; de paden staan in de tekst.
"""
u.check("Dockerfile bestaat", os.path.isfile(DOCKERFILE))
if not os.path.isfile(DOCKERFILE):
return
regels = zonder_commentaar(lees(DOCKERFILE))
copies = [r for r in regels if r.startswith("COPY ")]
def kopieert(bron, doel):
return any(bron in r and r.endswith(" " + doel) for r in copies)
u.check("nginx.conf gaat naar /etc/nginx/nginx.conf",
kopieert("tools/electrum-gate/nginx.conf", "/etc/nginx/nginx.conf"))
u.check("stream.conf gaat naar /etc/nginx/stream.conf.in",
kopieert("tools/electrum-gate/stream.conf", "/etc/nginx/stream.conf.in"),
"entrypoint.sh leest dat pad en vult het in naar stream.conf")
u.check("de pagina gaat naar de webroot",
kopieert("tools/electrum-gate/index.html", "/usr/share/nginx/html/index.html"))
u.check("het icoon komt uit de app-map",
kopieert("whatsnext-electrum-gate/icon.png", "/usr/share/nginx/html/icon.png"),
"de app-map houdt het origineel omdat het manifest ernaar wijst; "
"de image kopieert het, zodat er niet twee exemplaren zijn")
u.check("de agent gaat naar /app/agent.py",
kopieert("tools/electrum-gate/agent.py", "/app/agent.py"),
"de compose start hem op dat pad")
u.check("entrypoint.sh gaat mee en wordt uitvoerbaar",
kopieert("tools/electrum-gate/entrypoint.sh", "/entrypoint.sh")
and any(r.startswith("RUN chmod") and "/entrypoint.sh" in r for r in regels),
"git op Windows kent geen uitvoerbit, dus die moet de Dockerfile zetten")
u.check("python3 wordt geïnstalleerd",
any(r.startswith("RUN apk add") and "python3" in r for r in regels))
u.check("de basisimage is nginx op een vaste tag",
any(re.match(r"FROM nginx:\d+\.\d+-alpine$", r) for r in regels),
"nginx:alpine beweegt mee en is ook mét digest niet toegestaan; "
"zie Docs/Referenties/Images-pinnen.md")
u.check("het entrypoint van de nginx-image is uitgeschakeld",
"ENTRYPOINT []" in regels,
"anders draait het envsubst-script van die image mee, en dat is "
"precies het soort stille invulling dat dit project vreest")
# De compose noemt paden in de image; die moeten er zijn.
compose = zonder_commentaar(lees(COMPOSE))
u.check("de compose start de agent op het pad waar de Dockerfile hem neerzet",
any(r == "- /app/agent.py" for r in compose))
def test_compose_en_recept_lopen_gelijk(u):
"""Dezelfde image in beide services, met de tag uit build.sh.
Twee services die elk hun eigen tag hebben lopen uit elkaar bij de eerste
verhoging die iemand op één plek doet. En een tag die niet gelijk is aan
VERSION in build.sh betekent een image die nooit gebouwd is.
"""
compose = zonder_commentaar(lees(COMPOSE))
images = [r.split(":", 1)[1].strip() for r in compose if r.startswith("image:")]
u.check("de compose heeft twee images", len(images) == 2, "gevonden: %r" % images)
tags = set()
for image in images:
zonder_digest = image.split("@", 1)[0]
tags.add(zonder_digest)
u.check("en het is twee keer dezelfde", len(tags) == 1, "gevonden: %r" % sorted(tags))
versie = None
for regel in zonder_commentaar(lees(BUILD)):
if regel.startswith("VERSION="):
versie = regel.split("=", 1)[1].strip().strip('"')
u.check("build.sh heeft een VERSION", bool(versie))
if versie and tags:
tag = sorted(tags)[0]
u.check("de tag in de compose is VERSION uit build.sh",
tag.endswith(":" + versie),
"compose %r, build.sh %r" % (tag, versie))
# Wat er niet meer gemount mag worden: de programmabestanden. Zit er weer
# een mount van een los bestand uit de app-map in, dan is dat een terugval
# naar de template-tijd en bereikt een wijziging het apparaat via twee wegen.
losse = [r for r in compose if r.startswith("- ${APP_DATA_DIR}/")
and "/" not in r[len("- ${APP_DATA_DIR}/"):].split(":", 1)[0]]
u.check("geen losse bestanden uit de app-map meer gemount",
not losse, "gevonden: %r" % losse)
u.check("de server krijgt het backend-adres uit de omgeving",
compose.count("GATE_ELECTRUM_HOST: ${APP_ELECTRS_NODE_IP}") == 2
and compose.count("GATE_ELECTRUM_PORT: ${APP_ELECTRS_NODE_PORT}") == 2,
"agent én server hebben het nodig; entrypoint.sh vult er het "
"stream-blok mee in")
u.check("de agent krijgt de versie van het manifest door",
"GATE_APP_VERSION: ${APP_VERSION}" in compose,
"anders toont de kop van de pagina geen versie")
def test_uploadpad(u):
"""De drie dingen buiten de agent die uploaden mogelijk maken.
@@ -208,13 +340,12 @@ def test_uploadpad(u):
def test_verbindingsteller(u):
"""De teller in de nginx-container, en de poort in hex.
Hoort hier omdat het om hetzelfde `command`-blok gaat. Het gevaarlijke deel
is de hex: de teller zoekt in /proc/net/tcp op de poort in hexadecimale vorm,
en verandert de TLS-poort ooit, dan telt hij stil nul. Een teller die altijd
nul zegt, ziet eruit als "geen wallet verbonden" en niet als een fout.
Het gevaarlijke deel is de hex: de teller zoekt in /proc/net/tcp op de poort
in hexadecimale vorm, en verandert de TLS-poort ooit, dan telt hij stil nul.
Een teller die altijd nul zegt, ziet eruit als "geen wallet verbonden" en
niet als een fout.
"""
tekst = lees(COMPOSE)
regels = zonder_commentaar(tekst)
regels = zonder_commentaar(lees(ENTRYPOINT))
teller = [r for r in regels if "/proc/net/tcp" in r]
u.check("de teller leest /proc/net/tcp", bool(teller),
@@ -222,11 +353,12 @@ def test_verbindingsteller(u):
"staan; de agent zit in een andere netwerk-namespace")
u.check("en schrijft het aantal weg voor de agent",
any("/var/lib/gate/sessions" in r for r in regels))
any("/sessions" in r for r in regels))
# De poort uit de omgeving van de agent is de waarheid; de teller moet
# dezelfde poort in hex zoeken.
poorten = [r for r in regels if r.startswith("GATE_TLS_PORT:")]
compose = zonder_commentaar(lees(COMPOSE))
poorten = [r for r in compose if r.startswith("GATE_TLS_PORT:")]
u.check("GATE_TLS_PORT staat in de compose", len(poorten) == 1,
"gevonden: %r" % poorten)
if not poorten or not teller:
@@ -238,9 +370,13 @@ def test_verbindingsteller(u):
# backslash-vervolg en dan valt de hex op een eigen regel.
u.check("de teller zoekt de TLS-poort in hex (%d is %s)" % (poort, hexpoort),
(":" + hexpoort) in " ".join(regels),
"nergens in het command-blok staat :%s, dus telt de teller nul"
"nergens in entrypoint.sh staat :%s, dus telt de teller nul"
% hexpoort)
stream = zonder_commentaar(lees(STREAM_CONF))
u.check("en stream.conf luistert op diezelfde poort",
any(r == "listen %d ssl;" % poort for r in stream))
def test_data_onder_data(u):
"""Alles wat de app zelf schrijft staat onder `data/`.
@@ -262,14 +398,9 @@ def test_data_onder_data(u):
fout = []
for mount in mounts:
host = mount.strip('"').split(":")[0].replace("${APP_DATA_DIR}/", "")
# Een bestand naast de compose mag: dat zijn de ingevulde templates en het
# icoon, en die hóren daar omdat de whitelist ze daar verft. Een map die de
# app zelf vult, hoort onder data/.
if "." in os.path.basename(host):
continue
if not host.startswith("data/"):
fout.append(host)
u.check("elke map die de app zelf vult staat onder data/",
u.check("elke mount uit de app-map staat onder data/",
not fout, "deze niet: %r" % fout)
u.check("de gedeelde toestand staat onder data/",
@@ -382,17 +513,35 @@ def test_tagline_is_overal_dezelfde(u):
"de pagina noemt hem niet: %r" % tagline)
def test_pagina_zonder_invulling(u):
"""De pagina zit in de image en wordt door niemand meer ingevuld.
Een ${...} die blijft staan komt letterlijk in de browser terecht. En de twee
waarden die umbreld vroeger invulde, de versie en het backend-adres, moeten
nu uit status.json komen; anders is de kop leeg en het adres ook.
"""
tekst = lees(PAGINA)
u.check("de pagina bevat geen accolade-variabele", "${" not in tekst)
u.check("de kop haalt de versie uit status.json",
"s.version" in tekst and 'id="head-version"' in tekst)
u.check("het backend-adres komt uit status.json",
"backend.address" in tekst)
def main():
u = Uitslag()
test_pagina_hangt_niet_aan_een_certificaat(u)
test_stream_blok_staat_apart(u)
test_compose_wacht_niet_op_de_agent(u)
test_entrypoint_wacht_niet_op_de_agent(u)
test_dockerfile_neemt_alles_mee(u)
test_compose_en_recept_lopen_gelijk(u)
test_verbindingsteller(u)
test_uploadpad(u)
test_data_onder_data(u)
test_manifest_volgorde(u)
test_releasenotes_hebben_een_geschiedenis(u)
test_tagline_is_overal_dezelfde(u)
test_pagina_zonder_invulling(u)
return u.rapport()