7 Commits (43efac966bd7f18c77a261a5110bbc0162506622)
| Author | SHA1 | Message | Date |
|---|---|---|---|
|
|
2b79a6a2ba |
Der Tunnel haengt nicht mehr an einem huebschen Namen und nicht an einem Arbeiter
tests / pest (push) Failing after 9m49s
Details
tests / assets (push) Successful in 26s
Details
tests / release (push) Has been skipped
Details
Zwei Ursachen, eine davon habe ich mit v1.4.2 selbst gelegt. 1. DER NETZ-ALIAS. Damit in docker/nginx/default.conf `terminal:8082` stehen konnte statt des Namens des Nachbarcontainers, bekam queue-provisioning einen Eintrag unter `networks: aliases:`. Das ist Teil seiner NETZKONFIGURATION — und Compose baut einen Container neu, sobald die sich aendert. Neu gebaut heisst neue Adresse im Compose-Netz, heisst neu geschriebene Weiterleitungsregeln fuer den veroeffentlichten UDP-Port 51820. Auf dem liegt jede bestehende WireGuard-Sitzung. Der Preis fuer einen sprechenderen Namen in einer Konfigurationszeile war also ein Abriss saemtlicher Tunnel beim Ausrollen — der des Betreibers am Telefon wie der jedes Hosts. Alias entfernt, nginx spricht die Bruecke unter `queue-provisioning:8082` an. Nachgemessen: danach laesst `docker compose up -d` den Tunnel-Container unangetastet, und nginx erreicht die Bruecke weiterhin (426 statt 502). 2. DER ARBEITER ALS PROZESS 1. wg0 stand im Namensraum von queue-provisioning, und dessen Prozess 1 war `exec php artisan queue:work provisioning`. Endete der Arbeiter, endete der Container — und mit ihm der Namensraum und jede Sitzung darin. Ein Arbeiter endet oefter, als man denkt: `--timeout=2100` beendet ihn bei einem langen Provisionierungs-Schritt, ein fataler Fehler beendet ihn, ein Speicherlimit beendet ihn, `queue:restart` beendet ihn absichtlich. Aus jedem dieser vier Faelle wurde bisher "das VPN ist unzuverlaessig". Der Rumpf liegt jetzt in docker/provisioning-worker.sh: wg0 einmal hochziehen, danach den Arbeiter in einer Schleife halten, SIGTERM sauber weiterreichen. Nachgemessen: Arbeiter getoetet -> Container hat NULL Neustarts, wg0 steht weiter, Arbeiter ist von allein wieder da. Beide Male bleibt der Netz-Namensraum stehen. Damit verschwindet nebenbei auch der Grund, aus dem update.sh die Bruecke hinterher neu starten musste — die Zeile bleibt trotzdem, sie kostet nichts und traegt den Fall, dass der Container doch einmal neu gebaut wird. NICHT GETAGGT, mit Absicht: diese Aenderung fasst genau das an, was den Zugang des Betreibers traegt. Sie gehoert ausgerollt, wenn jemand davorsitzt und die Konsole des Anbieters als Rueckfahrkarte hat — nicht unbeaufsichtigt vom Update-Agenten, waehrend der Betreiber unterwegs ist. 2508 Tests gruen. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
|
|
|
c6403bf829 |
Terminal-Bruecke: ein Container, eine Aufgabe, ein Ticket
Der Beiwagen nach dem Vorbild von kuma-bridge: Python, ein Zweck, kein
Laravel. Er steht im Netz-Namensraum von queue-provisioning, weil dort wg0
lebt und nur von dort ein Host ueberhaupt erreichbar ist.
Drei Punkte, die das Review von Aufgabe 2 offen gelassen hat, sind hier
entschieden:
- Das Ticket reist als Sec-WebSocket-Protocol, nicht in der Adresszeile.
Die Adresse eines Upgrade-Antrags schreibt jeder Reverse Proxy mit, und
auf der Strecke stehen zwei, von denen nur einer aus diesem Repo
konfiguriert wird.
- /terminal/ws bleibt an der Wurzel, unabhaengig von AdminArea::prefix():
nginx kann die Konfiguration der Anwendung nicht lesen, der Ort muss ein
fester Text sein. In beiden Dateien steht die Begruendung.
- Die Bruecke sendet ausschliesslich Binaerrahmen.
Abweichungen vom Plan, jeweils gemessen statt vermutet:
- Der Schluessel in Redis traegt REDIS_PREFIX (clupilot-database-), nicht
CACHE_PREFIX: TerminalTicket schreibt ueber Redis::connection('cache'),
und phpredis stellt die Praefix-Option dieser Verbindung voran.
- Port 8082 statt 8081: im selben Namensraum lauscht der VPN-Gateway schon
auf VPN_HEALTH_PORT, und der zweite Zuhoerer auf einem Port stirbt.
- SSH ueber paramiko.Transport statt SSHClient. SSHClient prueft gegen
known_hosts, die dieser Container nicht hat und aus einem Fingerabdruck
auch nicht bilden kann; RejectPolicy verbaende nie, AutoAddPolicy
meldete sich mit einem Root-Schluessel an, bevor irgendetwas geprueft
ist. Der Transport erlaubt die richtige Reihenfolge: Handschlag,
Fingerabdruck, dann erst Anmeldung.
- Der Fingerabdruck wird gebildet wie in PhpseclibRemoteShell, nicht wie
bei OpenSSH: gehasht wird "<algorithmus> <base64-blob>", nicht der Blob.
- proxy_pass ueber eine Variable mit resolver, damit nginx nicht beim
Start scheitert, wenn die Bruecke gerade nicht laeuft.
- /terminal/ws antwortet auf einem oeffentlichen Hostnamen mit 404,
dieselbe Regel wie /admin eine Zeile darueber.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
|
|
07c51474d3 | Update drehte sich im Kreis: es ersetzte sich selbst mitten im Lauf; dazu eine Seite fuer offene Punkte | |
|
|
acc4193071 | www-data gehoerte sein eigenes Heimatverzeichnis nicht: npm ci brach das Deployment mit 243 ab | |
|
|
889b401faf |
Fix nine defects in the provisioning pipelines
The capacity park could not survive its own first poll. ReserveResources
polls every 120 s and declared a maxDuration of 60, and RunRunner measures
a step's budget from a started_at it deliberately does not reset on a poll
— so every re-entry was ruled timed out before the body ran, a timeout
consumes an attempt, and five of them failed a paid order in about six
minutes. The fourteen-day promise, the console's capacity queue and the
"go and buy a server" workflow were unreachable code. maxDuration is now
the whole park plus one poll interval, and a test drives a park through
the runner rather than calling execute() directly, which is why this was
invisible.
Every guest occ call ran as root. `docker compose exec` defaults to root
and the Nextcloud image's console.php exits 1 unless the caller owns
config/config.php, so all five call sites were failing on every instance.
There is now one builder, App\Support\NextcloudOcc, and a test refuses a
second: nothing in app/ may spell the invocation out by hand. deploy/
update.sh had learned this for our own container and nobody carried it
across.
RunAcceptanceChecks was terminal on the first no. certReachable() answers
false for a connect timeout as readily as for a missing certificate, so
one bad second ended a finished, certified Nextcloud as a failed order
with the instance released. The probes retry now and the run still fails
for good with the probe's own reason once the budget is spent; the two
facts a retry cannot change stay terminal.
The in-flight guard asked whether anything was running, not whether it
would do the work. App\Provisioning\WorkInFlight asks the second question,
reading the step lists the runner executes: a domain proven during a
restart is routed, and a storage pack booked during an address run is
delivered instead of charged monthly and forgotten.
The address pipeline ran its steps in the wrong order for the direction it
exists for. Nextcloud before the router in `address` and `plan-change`, as
in `customer`, because a run that got the certificate and then failed left
the customer's own domain serving an untrusted-domain error under a valid
certificate while the portal called it live. Safe in both directions; the
reasoning is written above the pipeline.
A stale guest_ip could never be corrected. ConfigureDnsAndTls compared the
hostnames but never the backend, and nothing re-read the address after the
build. It now records and compares `routed_backend`, and ConfigureNetwork
joins the `restart` pipeline — a cold boot is what moves a DHCP lease — and
asks for the address to be re-applied when the guest has actually moved.
Also: HetznerDnsClient::upsertRecord read only the first page of a zone
that pages at 100, so past a hundred records it created a second A record
for the same name and the cloud was up about half the time; the console's
retry left a live customer's order in `provisioning` for ever for any
maintenance pipeline; and a revived run's first pass burned an attempt on a
timeout that had already happened, because the clause written for it used
`??=`.
Seven test sites wrote config('provisioning.plans.*'), a key that has not
existed since the catalogue moved into the database. Every write was a
no-op, so each test proved something other than its name: the missing
template, the two snapshot regressions and the grandfathered-price scenario
are now actually constructed, ValidateOrder's reasons are asserted rather
than only its type, and the end-to-end run checks that the quota step
delivered something.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
|
|
6998a22527 |
Repair ownership by looking inside, not at the door
tests / pest (push) Failing after 8m2s
Details
tests / assets (push) Successful in 21s
Details
tests / release (push) Has been skipped
Details
The ownership repair added in 1.1.1 sampled the owner of each top-level directory and skipped the recursion when it already matched. node_modules was owned by www-data; node_modules/.vite-temp, left behind by an earlier root build, was not. So the repair walked past it, `npm run build` failed with EACCES, and the deployment stopped in maintenance mode — on the release whose whole point was that this could not happen. A directory's owner says nothing about its contents. It now walks each tree once with find and changes only the entries that are wrong, which is also cheaper than the detect-then-chown-everything it replaces. Verified against the shape that actually failed: a root-owned file inside a www-data-owned node_modules, repaired. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
|
|
|
00b9107ee3 |
Deploy as the application's user, and take back what root already took
tests / pest (push) Failing after 7m19s
Details
tests / assets (push) Successful in 19s
Details
tests / release (push) Has been skipped
Details
The 500 on the VPN config download was never in the VPN code. storage/logs/ laravel.log was owned by root, mode 644, since 27.07 04:16 — the moment a failed `artisan optimize` wrote its error there during a deployment. From then on the application could not append to its own log: Monolog threw on every attempt, and a throw while logging is a 500 on any page that logs, with nothing written down to say why. The error page said "Er wurde protokolliert". It was not. It surfaced on the config download because that is one of the few pages writing a log line on its way through — on success and on a wrong password alike. Pages that log nothing were unaffected, which is exactly why it looked like a VPN fault for days. `docker compose exec` is root unless told otherwise, and every deployment script relied on that default — including the agent's allowlist sync, which runs every minute. docker/entrypoint.sh had it right all along and drops to www-data for precisely these commands. Now so does everything else, help text included: telling an operator to run artisan as root is how the file ends up owned by root in the first place. update.sh also repairs what an earlier run left behind, before its first unprivileged step rather than after — a root-owned log file is not self-healing, the page that trips over it is nowhere near the deployment that caused it, and with in_app unprivileged a root-owned vendor/ would break the next composer step outright. tests/Feature/DeploymentRunsAsTheAppUserTest.php holds the line. It was checked against the previous commit and finds all nine places that were wrong. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |