feat(tools): remote <ziel> <aktion> als Vordertuer zum Server (D77-Nachtrag)
Deployen, Log ansehen, Dienst schalten und fragen was laeuft waren vier verschiedene Beschwoerungen, drei davon von Hand als ssh + systemctl. Jetzt: remote backend deploy|upload|setup|install-jdk|reset-password remote backend start|stop|restart|status|enable|disable remote backend log|info|documents|backup remote frontend deploy|preview|info remote ssh Die Skripte unter scripts/ bleiben die Implementierung und einzeln aufrufbar; tools/remote bringt nur mit, wofuer es bisher nichts gab. Wo ein Schalter noetig war, kam er ins Skript statt ins Werkzeug: --unit-only in deploy-backend.sh (sonst kennte eine zweite Stelle die Unit-Platzhalter) und --dry-run in deploy-prod.sh, das die Befoerderung ausdruecklich mit abschaltet. Neu ist die Sicherung: H2 haelt die Datei offen, also anhalten, holen, wieder starten (gemessen ~8 s Auszeit) - und das Archiv lesen, bevor der Befehl es behaelt. Gegenprobe von Hand: lokal ausgepackt, Backend mit --werkbaum.data-dir dagegen gestartet, es liefert genau die Dokumente des Servers. .envrc legt tools/ auf den PATH (direnv), 217 Plan-Knoten, 0 Warnungen.
This commit is contained in:
@@ -0,0 +1,16 @@
|
|||||||
|
# direnv-Konfiguration.
|
||||||
|
#
|
||||||
|
# Legt `tools/` auf den PATH, solange man in diesem Verzeichnisbaum steht — dann
|
||||||
|
# genügt `remote backend deploy` statt `tools/remote backend deploy`. Wirkt nur
|
||||||
|
# hier und darunter, die globale Shell-Konfiguration bleibt unangetastet.
|
||||||
|
#
|
||||||
|
# Einmal pro Arbeitskopie freizugeben (direnv lädt eine Datei erst, wenn man ihr
|
||||||
|
# traut) — und erneut, sobald sich diese Datei ändert:
|
||||||
|
#
|
||||||
|
# direnv allow
|
||||||
|
#
|
||||||
|
# Bewusst `tools` und nicht `scripts`: Auf dem PATH hieße der Befehl sonst
|
||||||
|
# `deploy-prod.sh`. Die Skripte bleiben, wo sie sind, und einzeln aufrufbar;
|
||||||
|
# `tools/remote` ist die Vordertür davor.
|
||||||
|
|
||||||
|
PATH_add tools
|
||||||
@@ -69,6 +69,10 @@ node_modules/
|
|||||||
.env.local
|
.env.local
|
||||||
*.local
|
*.local
|
||||||
|
|
||||||
|
# Datenbank-Sicherungen vom Server (`remote backend backup`). Sie enthalten
|
||||||
|
# fremde Pläne — sie gehören nie ins Repository, auch nicht versehentlich.
|
||||||
|
backups/
|
||||||
|
|
||||||
# === Temporäres ===
|
# === Temporäres ===
|
||||||
tmp/
|
tmp/
|
||||||
*.tmp
|
*.tmp
|
||||||
|
|||||||
@@ -9,6 +9,11 @@ Integrations-Backend.
|
|||||||
- `backend/` — Kotlin/Spring-Boot-Anwendung (Taiga-Integration, Persistenz).
|
- `backend/` — Kotlin/Spring-Boot-Anwendung (Taiga-Integration, Persistenz).
|
||||||
Eigene Regeln: backend/CLAUDE.md
|
Eigene Regeln: backend/CLAUDE.md
|
||||||
- `docs/` — Projektdokumente · `brand/` — Marke (brand/BRAND.md)
|
- `docs/` — Projektdokumente · `brand/` — Marke (brand/BRAND.md)
|
||||||
|
- `tools/remote` — alles, was auf dem Server passiert, als Ziel und Aktion
|
||||||
|
(`remote backend deploy`, `remote frontend preview`, …; `remote --help`).
|
||||||
|
Die Arbeit machen weiterhin die Skripte unter `scripts/` — `remote` ist die
|
||||||
|
Vordertür davor. Braucht ein Skript einen neuen Schalter, kommt der **ins
|
||||||
|
Skript**, nicht ins Werkzeug (D77-Nachtrag).
|
||||||
|
|
||||||
## Verbindliche Referenzen
|
## Verbindliche Referenzen
|
||||||
- Sprachdefinition: @docs/SPEC.md — die Notation ist der gemeinsame Vertrag
|
- Sprachdefinition: @docs/SPEC.md — die Notation ist der gemeinsame Vertrag
|
||||||
|
|||||||
@@ -246,6 +246,31 @@ repository, while the **version number** links to that exact commit
|
|||||||
"GitHub Actions". The repo must be **public** for this (GitHub Pages via Actions
|
"GitHub Actions". The repo must be **public** for this (GitHub Pages via Actions
|
||||||
is only available for private repos on a paid plan).
|
is only available for private repos on a paid plan).
|
||||||
|
|
||||||
|
### One command for the server: `remote`
|
||||||
|
|
||||||
|
Everything that happens on the server is reachable as **target and action**:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
remote backend deploy # build, upload, unit, restart, then probe
|
||||||
|
remote backend log # follow backend.log
|
||||||
|
remote backend status # systemctl --user status
|
||||||
|
remote backend info # which build is running, through the proxy
|
||||||
|
remote backend backup # stop, fetch the database, start again
|
||||||
|
remote backend reset-password # asks, hashes it on the server, verifies
|
||||||
|
remote frontend deploy # promote, build, assemble, mirror
|
||||||
|
remote frontend preview # what would change — writes nothing
|
||||||
|
remote frontend info # which version is out there
|
||||||
|
remote ssh # a shell on the server
|
||||||
|
```
|
||||||
|
|
||||||
|
`remote --help` lists them all. With [direnv](https://direnv.net) the repo's
|
||||||
|
`.envrc` puts `tools/` on `PATH` (once per checkout: `direnv allow`), so the
|
||||||
|
command needs no path; otherwise call `tools/remote`.
|
||||||
|
|
||||||
|
The scripts under `scripts/` remain the implementation and stay usable on their
|
||||||
|
own — `remote` is the front door and brings along only what had no script
|
||||||
|
before: the systemd verbs, the log, the state queries and the backup.
|
||||||
|
|
||||||
### Stable instance
|
### Stable instance
|
||||||
|
|
||||||
`scripts/deploy-prod.sh` mirrors a badge-free production build to a server over
|
`scripts/deploy-prod.sh` mirrors a badge-free production build to a server over
|
||||||
@@ -268,12 +293,16 @@ footer version link points at a commit GitHub does not know yet.
|
|||||||
The backend is a separate deploy, because it is a service rather than files:
|
The backend is a separate deploy, because it is a service rather than files:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
scripts/install-jdk.sh # once: a JDK 21 into the server's home
|
remote backend install-jdk # once: a JDK 21 into the server's home
|
||||||
scripts/deploy-backend.sh # build, upload, systemd user unit, restart
|
remote backend deploy # build, upload, systemd user unit, restart
|
||||||
scripts/reset-password.sh # asks for the master password, hashes it there
|
remote backend reset-password # asks for the master password, hashes it there
|
||||||
scripts/deploy-prod.sh # the editor — also writes the /api/ proxy rule
|
remote frontend deploy # the editor — also writes the /api/ proxy rule
|
||||||
```
|
```
|
||||||
|
|
||||||
|
(The same four as `scripts/install-jdk.sh`, `scripts/deploy-backend.sh`,
|
||||||
|
`scripts/reset-password.sh` and `scripts/deploy-prod.sh`, if you'd rather call
|
||||||
|
them directly.)
|
||||||
|
|
||||||
`GET /api/v1/info` answers with name, version and build time — that is the
|
`GET /api/v1/info` answers with name, version and build time — that is the
|
||||||
liveness check, for the deploy and for monitoring. Expecting a **404** from a
|
liveness check, for the deploy and for monitoring. Expecting a **404** from a
|
||||||
document that does not exist would be a poor assurance: a misconfigured proxy
|
document that does not exist would be a poor assurance: a misconfigured proxy
|
||||||
|
|||||||
+19
-11
@@ -261,25 +261,33 @@ beim Start aus der IDE —, steht dort `unbekannt` statt eines Fehlers.
|
|||||||
|
|
||||||
## Betrieb auf der stabilen Instanz
|
## Betrieb auf der stabilen Instanz
|
||||||
|
|
||||||
`scripts/deploy-backend.sh` (im Repo-Wurzelordner) baut das Fat-Jar, legt es
|
`remote backend deploy` (aus dem Repo-Wurzelordner; ohne direnv
|
||||||
samt systemd-User-Unit ins Home des Servers und startet den Dienst neu;
|
`tools/remote …`) baut das Fat-Jar, legt es samt systemd-User-Unit ins Home des
|
||||||
`scripts/install-jdk.sh` bringt einmalig ein JDK 21 dorthin. Der Dienst lauscht
|
Servers und startet den Dienst neu; `remote backend install-jdk` bringt einmalig
|
||||||
nur auf `127.0.0.1` — von außen kommt man über die Proxy-Regel in
|
ein JDK 21 dorthin. Der Dienst lauscht nur auf `127.0.0.1` — von außen kommt man
|
||||||
`scripts/prod.htaccess`, die der Frontend-Deploy mitspiegelt.
|
über die Proxy-Regel in `scripts/prod.htaccess`, die der Frontend-Deploy
|
||||||
|
mitspiegelt.
|
||||||
|
|
||||||
|
Dahinter stehen unverändert `scripts/deploy-backend.sh` und
|
||||||
|
`scripts/install-jdk.sh`; `remote --help` zeigt alle Ziele und Aktionen.
|
||||||
|
|
||||||
- **Speicher:** `-Xmx192m -Xms48m` plus Freiraum-Verhältnisse; gemessen rund
|
- **Speicher:** `-Xmx192m -Xms48m` plus Freiraum-Verhältnisse; gemessen rund
|
||||||
174 MB RSS gegen 291 MB ohne Angaben. Nach einem GC leben ~45 MB. Zu wenig
|
174 MB RSS gegen 291 MB ohne Angaben. Nach einem GC leben ~45 MB. Zu wenig
|
||||||
Luft? `BACKEND_XMX` in `.env`.
|
Luft? `BACKEND_XMX` in `.env`.
|
||||||
- **Master-Passwort:** `scripts/reset-password.sh` fragt es verdeckt ab, hasht
|
- **Master-Passwort:** `remote backend reset-password` fragt es verdeckt ab,
|
||||||
es auf dem Server und prüft selbst nach, ob Hash und Passwort zueinander
|
hasht es auf dem Server und prüft selbst nach, ob Hash und Passwort
|
||||||
passen. Gespeichert wird nur der Hash (`<BACKEND_DIR>/env`, Modus 600); ohne
|
zueinander passen. Gespeichert wird nur der Hash (`<BACKEND_DIR>/env`, Modus
|
||||||
ihn bleibt `GET /documents` gesperrt.
|
600); ohne ihn bleibt `GET /documents` gesperrt. Abrufen lässt sich die Liste
|
||||||
|
mit `remote backend documents`.
|
||||||
- **Datenbank:** H2 im Dateimodus unter `<BACKEND_DIR>/data/`. Ein Deploy
|
- **Datenbank:** H2 im Dateimodus unter `<BACKEND_DIR>/data/`. Ein Deploy
|
||||||
fasst das Verzeichnis nicht an (kein `--delete`). Umstieg auf das dort
|
fasst das Verzeichnis nicht an (kein `--delete`). Umstieg auf das dort
|
||||||
laufende PostgreSQL: JDBC-URL in der `application.yaml` tauschen und den
|
laufende PostgreSQL: JDBC-URL in der `application.yaml` tauschen und den
|
||||||
Treiber ergänzen — Schema und Code bleiben.
|
Treiber ergänzen — Schema und Code bleiben.
|
||||||
- **Nachsehen:** `systemctl --user status werkbaum-backend`,
|
- **Sichern:** `remote backend backup`. H2 hält die Datei offen, solange der
|
||||||
`tail -f <BACKEND_DIR>/backend.log`.
|
Dienst läuft — gesichert wird deshalb mit kurzem Anhalten, und das Archiv
|
||||||
|
wird nach dem Herunterladen gelesen, bevor der Befehl es behält.
|
||||||
|
- **Nachsehen:** `remote backend status`, `remote backend log`,
|
||||||
|
`remote backend info`.
|
||||||
|
|
||||||
Begründungen: docs/DECISIONS.md D77 (und D76-Nachträge 1–3 zur Vermessung der
|
Begründungen: docs/DECISIONS.md D77 (und D76-Nachträge 1–3 zur Vermessung der
|
||||||
Zielumgebung).
|
Zielumgebung).
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ reverse.
|
|||||||
- The backend can be deployed to a server: its own JDK, a systemd service, and Apache passing `/api/` on
|
- The backend can be deployed to a server: its own JDK, a systemd service, and Apache passing `/api/` on
|
||||||
- "Put on the server" in the document menu turns a local plan into a shared one and puts the link in the address bar
|
- "Put on the server" in the document menu turns a local plan into a shared one and puts the link in the address bar
|
||||||
- A service endpoint `/api/v1/info` says which build is running — for deploys and monitoring
|
- A service endpoint `/api/v1/info` says which build is running — for deploys and monitoring
|
||||||
|
- One `remote` command drives the server: deploy, log, service state, and a database backup that is read back before it is kept
|
||||||
|
|
||||||
## 2026-08-25
|
## 2026-08-25
|
||||||
|
|
||||||
|
|||||||
@@ -6516,3 +6516,85 @@ Probe mit 404 und ist von außen nicht erreichbar (`server.address=127.0.0.1`,
|
|||||||
gegengeprüft über die LAN-Adresse). Der Deploy selbst — SSH, `systemctl`, die
|
gegengeprüft über die LAN-Adresse). Der Deploy selbst — SSH, `systemctl`, die
|
||||||
Proxy-Regel im Betrieb — läuft erst, wenn jemand ihn startet. Das ist dieselbe
|
Proxy-Regel im Betrieb — läuft erst, wenn jemand ihn startet. Das ist dieselbe
|
||||||
Grenze wie in D25 und D72: Was die Umgebung stellt, stellt der Emulator nicht.
|
Grenze wie in D25 und D72: Was die Umgebung stellt, stellt der Emulator nicht.
|
||||||
|
|
||||||
|
**Nachtrag — `tools/remote`: eine Vordertür, Ziel und Aktion (2026-08-26).**
|
||||||
|
Die Skripte deckten den Deploy ab und sonst nichts. Alles Übrige — Log
|
||||||
|
ansehen, Dienst schalten, fragen was läuft — war ein von Hand getipptes
|
||||||
|
`ssh … systemctl --user …`, jedes Mal samt der `XDG_RUNTIME_DIR`-Falle. Nach
|
||||||
|
dem Muster eines anderen Projekts des Nutzers gibt es dafür jetzt einen
|
||||||
|
Befehl: `remote <ziel> <aktion>`, mit `backend`, `frontend` und `ssh` als
|
||||||
|
Zielen. Eine `.envrc` legt `tools/` auf den PATH (direnv), sodass `remote`
|
||||||
|
ohne Pfad genügt.
|
||||||
|
|
||||||
|
**Die Skripte bleiben die Implementierung, `remote` ist die Vordertür.** Sie
|
||||||
|
sind in beiden READMEs und in diesem Eintrag beschrieben, einzeln aufrufbar
|
||||||
|
und in Vorbereitung eines CI-Laufs nützlich; sie in das Werkzeug zu ziehen
|
||||||
|
hätte einen großen Diff für keinen Gewinn gebracht. `remote` bringt nur mit,
|
||||||
|
wofür es bisher gar nichts gab: die systemd-Verben, `log`, `info`,
|
||||||
|
`documents`, `backup`. Der Preis ist benannt — zwei Namen für dieselbe Sache,
|
||||||
|
deshalb nennen die READMEs jetzt `remote …` zuerst.
|
||||||
|
|
||||||
|
**Wo es einen Schalter brauchte, kam der ins Skript, nicht ins Werkzeug.**
|
||||||
|
`remote backend setup` schreibt nur die Unit neu; dafür hat
|
||||||
|
`deploy-backend.sh` ein `--unit-only` bekommen, statt dass `tools/remote`
|
||||||
|
die Platzhalter der Vorlage ein zweites Mal ersetzt. Genau diese Verdopplung
|
||||||
|
ist in D16 schon einmal teuer geworden (dieselben `sed`-Regeln in Workflow
|
||||||
|
und Skript). Ebenso `remote frontend preview` → `deploy-prod.sh --dry-run`.
|
||||||
|
|
||||||
|
**`--dry-run` schaltet die Beförderung ausdrücklich mit ab.** Sie läuft als
|
||||||
|
Schritt 0, also **vor** der rsync-Vorschau, und macht einen Commit (D30) —
|
||||||
|
ein Probelauf, der etwas schreibt, ist keiner. Gebaut und zusammengestellt
|
||||||
|
wird trotzdem, sonst wüsste der Vergleich nicht, wogegen er läuft.
|
||||||
|
Nachgemessen: HEAD und der Plan bleiben unangetastet.
|
||||||
|
|
||||||
|
**Sichern heißt anhalten.** H2 hält die Datei offen, solange der Dienst
|
||||||
|
läuft; eine Kopie im Betrieb kann zerrissen sein, und eine Sicherung, der man
|
||||||
|
nicht trauen kann, ist keine. `remote backend backup` hält den Dienst an,
|
||||||
|
holt `data/` als Tar-Strom und startet ihn wieder — gemessen rund 8 s Auszeit.
|
||||||
|
Zwei Feinheiten, die beide lautlos zuschlagen würden: Der Strom geht über
|
||||||
|
stdout in die Datei, **alle** Meldungen müssen deshalb nach stderr (sonst
|
||||||
|
landen sie im Archiv), und der Trap fängt `PIPE` mit ab — bricht die lokale
|
||||||
|
Seite weg, bliebe der Dienst sonst ausgerechnet dann unten.
|
||||||
|
|
||||||
|
**Und der Befehl liest das Archiv, bevor er es behält.** Erst nach
|
||||||
|
`tar tzf` und dem Nachweis, dass eine H2-Datei darin liegt, wird aus
|
||||||
|
`.teil` die endgültige Datei. Die eigentliche Gegenprobe lief einmal von
|
||||||
|
Hand und gehört hierher, weil sie die Zusage prüft und nicht die Mechanik:
|
||||||
|
Das Archiv lokal ausgepackt, das Backend mit `--werkbaum.data-dir` dagegen
|
||||||
|
gestartet — es kommt hoch und liefert genau die Dokumente, die auch auf dem
|
||||||
|
Server stehen. Ein Archiv, das nie jemand geöffnet hat, ist eine Hoffnung.
|
||||||
|
|
||||||
|
**`backend info` fragt über die öffentliche Adresse**, nicht am Dienst
|
||||||
|
vorbei: So ist die Proxy-Regel mitgeprüft. Die Domain wird aus dem
|
||||||
|
rsync-Ziel des Frontends abgelesen (`…/doms/<domain>/…`); ohne sie fragt der
|
||||||
|
Befehl direkt an `127.0.0.1` **und sagt, dass er es tut**. `frontend info`
|
||||||
|
liest den Versions-Link aus dem ausgelieferten Footer (D16) und vergleicht
|
||||||
|
den Commit mit dem eigenen HEAD — die einzige Stelle, an der die Datei
|
||||||
|
selbst sagt, was sie ist.
|
||||||
|
|
||||||
|
**`documents` bekommt das Passwort nie über die Kommandozeile.** Ohne
|
||||||
|
Angabe fragt curl selbst danach; mit `--from-env` kommt es als
|
||||||
|
`curl -K -` über stdin. Die Begründung steht schon im
|
||||||
|
`reset-password`-Nachtrag oben und gilt hier wörtlich: Argumente stehen in
|
||||||
|
der Prozessliste, und die Shell verändert das Passwort vorher.
|
||||||
|
|
||||||
|
**Gemessen statt geraten, zwei Kleinigkeiten am Rand:** Ein `frontend log`
|
||||||
|
gibt es **nicht** — `~/doms/werkbaum.javagil.de/var/` ist leer, der Managed
|
||||||
|
Webspace reicht die Apache-Logs nicht ins Home (nur monatliche
|
||||||
|
`~/var/domaintraffic-*.log`); an seine Stelle tritt `frontend info`. Und
|
||||||
|
`ssh -t` gehört nur dorthin, wo es ein Terminal gibt: Sonst steht
|
||||||
|
„Pseudo-terminal will not be allocated" als erste Zeile mitten im Log
|
||||||
|
(beim Bauen gemessen).
|
||||||
|
|
||||||
|
**Der Befehl wartet, bis der Dienst wieder antwortet.** Die erste Fassung von
|
||||||
|
`backup` meldete „Dienst wieder gestartet" und das nächste `info` bekam ein
|
||||||
|
503 — der Start dauert rund 8 s. Ein Werkzeug, das eine Sache meldet, die
|
||||||
|
gleich darauf nicht stimmt, ist schlechter als eines, das schweigt.
|
||||||
|
|
||||||
|
**Nachgemessen** gegen die produktive Instanz: `backend info` (200 samt
|
||||||
|
Version), `frontend info` (1.1.162, Commit = HEAD), `backend status`,
|
||||||
|
`backend log`, `backend documents --from-env` (liefert die Dokumente),
|
||||||
|
`backend setup -y` (Unit neu, Jar unangetastet — mtime unverändert),
|
||||||
|
`backend restart`, `backend backup` samt lokalem Wiederanlauf,
|
||||||
|
`frontend preview` (nichts geschrieben, HEAD unverändert) und die
|
||||||
|
Fehlerpfade (Exit-Code 2 bei unbekanntem Ziel und fehlender Aktion).
|
||||||
|
|||||||
@@ -107,10 +107,12 @@
|
|||||||
- [^] #bld.pwa.sw: A service worker that serves the app offline (S) %% network-first, cache as fallback
|
- [^] #bld.pwa.sw: A service worker that serves the app offline (S) %% network-first, cache as fallback
|
||||||
- [^] #bld.pwa.update: The reload notice stays truthful under the worker (S) %% see D45/D73
|
- [^] #bld.pwa.update: The reload notice stays truthful under the worker (S) %% see D45/D73
|
||||||
- [^] #bld.pwa.files: Double-click a .werkbaum file, save without any dialog (S) :#ed.files.inplace %% file_handlers + launchQueue
|
- [^] #bld.pwa.files: Double-click a .werkbaum file, save without any dialog (S) :#ed.files.inplace %% file_handlers + launchQueue
|
||||||
- [^] #bld.backend: Ship the backend as a service of its own (M)
|
- [^] #bld.backend: Ship the backend as a service of its own (L)
|
||||||
- [^] #bld.backend.jdk: Its own JDK 21 in the server's home (S) %% only 17 is installed there
|
- [^] #bld.backend.jdk: Its own JDK 21 in the server's home (S) %% only 17 is installed there
|
||||||
- [^] #bld.backend.unit: A systemd user unit that survives the session (S)
|
- [^] #bld.backend.unit: A systemd user unit that survives the session (S)
|
||||||
- [^] #bld.backend.proxy: Apache passes /api/ on, in .htaccess (XS) %% measured: the P flag is allowed
|
- [^] #bld.backend.proxy: Apache passes /api/ on, in .htaccess (XS) %% measured: the P flag is allowed
|
||||||
|
- [x] #bld.backend.remote: One command for everything on the server (S) %% remote <target> <action>
|
||||||
|
- [x] #bld.backend.backup: Save the database, and prove it can be read back (XS)
|
||||||
- [-] #bld.ghpages: Deploy through a gh-pages branch (S) %% GitHub Actions instead
|
- [-] #bld.ghpages: Deploy through a gh-pages branch (S) %% GitHub Actions instead
|
||||||
- [-] #bld.gfonts: Fonts from Google (XS) %% self-hosted, no third-party request
|
- [-] #bld.gfonts: Fonts from Google (XS) %% self-hosted, no third-party request
|
||||||
- [ ] #be: Backend (XL)
|
- [ ] #be: Backend (XL)
|
||||||
@@ -749,6 +751,16 @@
|
|||||||
.htaccess and the vhost directory is out of reach. Measured on the target: a
|
.htaccess and the vhost directory is out of reach. Measured on the target: a
|
||||||
request is held open for 30 seconds, which is what the change feed needs.
|
request is held open for 30 seconds, which is what the change feed needs.
|
||||||
|
|
||||||
|
#bld.backend.remote
|
||||||
|
Deploying, reading the log, asking what is running and switching the service
|
||||||
|
were four different incantations, three of them typed by hand as ssh plus
|
||||||
|
systemctl. One front door names them all as target and action.
|
||||||
|
|
||||||
|
#bld.backend.backup
|
||||||
|
The database is a file in a home directory, and H2 holds it open, so a copy
|
||||||
|
taken while the service runs can be torn. Stop, fetch, start again — and read
|
||||||
|
the archive back before trusting it.
|
||||||
|
|
||||||
#bld.ghpages
|
#bld.ghpages
|
||||||
Rejected: the official Actions route needs no extra branch and no deploy
|
Rejected: the official Actions route needs no extra branch and no deploy
|
||||||
key.
|
key.
|
||||||
|
|||||||
+25
-14
@@ -8,11 +8,15 @@
|
|||||||
# `scripts/deploy-prod.sh` mitspiegelt (D76-Nachtrag 2).
|
# `scripts/deploy-prod.sh` mitspiegelt (D76-Nachtrag 2).
|
||||||
#
|
#
|
||||||
# Verwendung:
|
# Verwendung:
|
||||||
# scripts/deploy-backend.sh [-y] [--no-build] [--no-restart] [ssh-ziel]
|
# scripts/deploy-backend.sh [-y] [--no-build] [--no-restart] [--unit-only] [ssh-ziel]
|
||||||
#
|
#
|
||||||
# -y ohne Rückfrage spiegeln und neu starten
|
# -y ohne Rückfrage spiegeln und neu starten
|
||||||
# --no-build vorhandenes Jar nehmen (z. B. Wiederholung eines Deploys)
|
# --no-build vorhandenes Jar nehmen (z. B. Wiederholung eines Deploys)
|
||||||
# --no-restart nur hochladen, Dienst nicht anfassen
|
# --no-restart nur hochladen, Dienst nicht anfassen
|
||||||
|
# --unit-only nur die systemd-Unit aus der Vorlage neu schreiben und laden;
|
||||||
|
# kein Bauen, kein Jar. Für den Fall, dass sich Port, Speicher
|
||||||
|
# oder die Vorlage selbst geändert haben. Die Platzhalter kennt
|
||||||
|
# damit weiterhin genau eine Stelle — dieses Skript.
|
||||||
#
|
#
|
||||||
# Konfiguration aus der git-ignorierten `.env` (Vorlage: .env.example):
|
# Konfiguration aus der git-ignorierten `.env` (Vorlage: .env.example):
|
||||||
# BACKEND_SSH user@host — Pflicht (oder als Argument)
|
# BACKEND_SSH user@host — Pflicht (oder als Argument)
|
||||||
@@ -37,12 +41,14 @@ set -euo pipefail
|
|||||||
YES=0
|
YES=0
|
||||||
BUILD=1
|
BUILD=1
|
||||||
RESTART=1
|
RESTART=1
|
||||||
|
UNIT_ONLY=0
|
||||||
SSH_TARGET=""
|
SSH_TARGET=""
|
||||||
for arg in "$@"; do
|
for arg in "$@"; do
|
||||||
case "$arg" in
|
case "$arg" in
|
||||||
-y|--yes) YES=1 ;;
|
-y|--yes) YES=1 ;;
|
||||||
--no-build) BUILD=0 ;;
|
--no-build) BUILD=0 ;;
|
||||||
--no-restart) RESTART=0 ;;
|
--no-restart) RESTART=0 ;;
|
||||||
|
--unit-only) UNIT_ONLY=1; BUILD=0 ;;
|
||||||
-h|--help)
|
-h|--help)
|
||||||
awk 'NR>2 { if ($0 ~ /^#/) { sub(/^# ?/, ""); print } else exit }' "$0"
|
awk 'NR>2 { if ($0 ~ /^#/) { sub(/^# ?/, ""); print } else exit }' "$0"
|
||||||
exit 0 ;;
|
exit 0 ;;
|
||||||
@@ -90,18 +96,22 @@ echo "==> Ziel ${SSH_TARGET}:~/${DIR#/}, Port ${PORT}, -Xmx${XMX}"
|
|||||||
|
|
||||||
# ---- 1) Bauen ----
|
# ---- 1) Bauen ----
|
||||||
JAR=""
|
JAR=""
|
||||||
if [ "$BUILD" -eq 1 ]; then
|
if [ "$UNIT_ONLY" -eq 1 ]; then
|
||||||
echo "==> ./gradlew bootJar (mit Tests)"
|
echo "==> Nur die Unit (--unit-only)"
|
||||||
(cd backend && ./gradlew --quiet check bootJar)
|
|
||||||
else
|
else
|
||||||
echo "==> Build übersprungen (--no-build)"
|
if [ "$BUILD" -eq 1 ]; then
|
||||||
|
echo "==> ./gradlew bootJar (mit Tests)"
|
||||||
|
(cd backend && ./gradlew --quiet check bootJar)
|
||||||
|
else
|
||||||
|
echo "==> Build übersprungen (--no-build)"
|
||||||
|
fi
|
||||||
|
JAR="$(ls -1t backend/build/libs/*.jar 2>/dev/null | grep -v -- '-plain\.jar$' | head -1 || true)"
|
||||||
|
if [ -z "$JAR" ]; then
|
||||||
|
echo "Kein Fat-Jar in backend/build/libs — ohne --no-build versuchen." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo " ${JAR} ($(du -h "$JAR" | cut -f1))"
|
||||||
fi
|
fi
|
||||||
JAR="$(ls -1t backend/build/libs/*.jar 2>/dev/null | grep -v -- '-plain\.jar$' | head -1 || true)"
|
|
||||||
if [ -z "$JAR" ]; then
|
|
||||||
echo "Kein Fat-Jar in backend/build/libs — ohne --no-build versuchen." >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
echo " ${JAR} ($(du -h "$JAR" | cut -f1))"
|
|
||||||
|
|
||||||
# ---- 2) Unit-Datei aus der Vorlage ----
|
# ---- 2) Unit-Datei aus der Vorlage ----
|
||||||
# Die Platzhalter werden hier ersetzt, nicht auf dem Server: So steht im
|
# Die Platzhalter werden hier ersetzt, nicht auf dem Server: So steht im
|
||||||
@@ -114,11 +124,11 @@ sed -e "s#__JAVA__#${JAVA_UNIT}#g" \
|
|||||||
-e "s#__XMX__#${XMX}#g" \
|
-e "s#__XMX__#${XMX}#g" \
|
||||||
-e "s#__PORT__#${PORT}#g" \
|
-e "s#__PORT__#${PORT}#g" \
|
||||||
scripts/werkbaum-backend.service > "$STAGE/werkbaum-backend.service"
|
scripts/werkbaum-backend.service > "$STAGE/werkbaum-backend.service"
|
||||||
cp "$JAR" "$STAGE/werkbaum-backend.jar"
|
[ "$UNIT_ONLY" -eq 1 ] || cp "$JAR" "$STAGE/werkbaum-backend.jar"
|
||||||
|
|
||||||
if [ "$YES" -ne 1 ]; then
|
if [ "$YES" -ne 1 ]; then
|
||||||
echo "==> Es werden übertragen:"
|
echo "==> Es werden übertragen:"
|
||||||
echo " werkbaum-backend.jar -> ${DIR}/"
|
[ "$UNIT_ONLY" -eq 1 ] || echo " werkbaum-backend.jar -> ${DIR}/"
|
||||||
echo " werkbaum-backend.service -> ~/.config/systemd/user/"
|
echo " werkbaum-backend.service -> ~/.config/systemd/user/"
|
||||||
[ "$RESTART" -eq 1 ] && echo " und der Dienst neu gestartet."
|
[ "$RESTART" -eq 1 ] && echo " und der Dienst neu gestartet."
|
||||||
printf '==> Weiter? [y/N] '
|
printf '==> Weiter? [y/N] '
|
||||||
@@ -132,7 +142,8 @@ fi
|
|||||||
# räumt nicht auf.
|
# räumt nicht auf.
|
||||||
echo "==> Übertragen"
|
echo "==> Übertragen"
|
||||||
ssh "$SSH_TARGET" "mkdir -p \"$DIR_SH\" \"\$HOME/.config/systemd/user\""
|
ssh "$SSH_TARGET" "mkdir -p \"$DIR_SH\" \"\$HOME/.config/systemd/user\""
|
||||||
rsync -az --chmod=F644 "$STAGE/werkbaum-backend.jar" "$SSH_TARGET:$DIR_RSYNC/werkbaum-backend.jar"
|
[ "$UNIT_ONLY" -eq 1 ] || \
|
||||||
|
rsync -az --chmod=F644 "$STAGE/werkbaum-backend.jar" "$SSH_TARGET:$DIR_RSYNC/werkbaum-backend.jar"
|
||||||
rsync -az --chmod=F644 "$STAGE/werkbaum-backend.service" \
|
rsync -az --chmod=F644 "$STAGE/werkbaum-backend.service" \
|
||||||
"$SSH_TARGET:~/.config/systemd/user/werkbaum-backend.service"
|
"$SSH_TARGET:~/.config/systemd/user/werkbaum-backend.service"
|
||||||
|
|
||||||
|
|||||||
+14
-1
@@ -11,12 +11,17 @@
|
|||||||
# Zielverzeichnis. `--delete`: am Ziel bleibt nichts Altes stehen.
|
# Zielverzeichnis. `--delete`: am Ziel bleibt nichts Altes stehen.
|
||||||
#
|
#
|
||||||
# Verwendung:
|
# Verwendung:
|
||||||
# scripts/deploy-prod.sh [-y] [--no-promote] [rsync-ziel]
|
# scripts/deploy-prod.sh [-y] [--no-promote] [--dry-run] [rsync-ziel]
|
||||||
#
|
#
|
||||||
# -y ohne Rückfrage befördern und spiegeln (sonst erst Vorschau
|
# -y ohne Rückfrage befördern und spiegeln (sonst erst Vorschau
|
||||||
# via --dry-run + Nachfrage)
|
# via --dry-run + Nachfrage)
|
||||||
# --no-promote Beförderungsschritt überspringen (z. B. Wiederholung eines
|
# --no-promote Beförderungsschritt überspringen (z. B. Wiederholung eines
|
||||||
# Deploys, der schon befördert hat)
|
# Deploys, der schon befördert hat)
|
||||||
|
# --dry-run nur zeigen, was sich am Ziel änderte, und dann aufhören.
|
||||||
|
# Schaltet die Beförderung ausdrücklich mit ab: die macht einen
|
||||||
|
# Commit, und ein Probelauf, der etwas schreibt, ist keiner.
|
||||||
|
# Gebaut und zusammengestellt wird trotzdem — sonst wüsste der
|
||||||
|
# Vergleich nicht, wogegen er läuft.
|
||||||
#
|
#
|
||||||
# Das Ziel ist entweder das Argument ODER — wenn keins angegeben ist — die
|
# Das Ziel ist entweder das Argument ODER — wenn keins angegeben ist — die
|
||||||
# Variable DEPLOY_TARGET aus der git-ignorierten Datei .env im Repo-Wurzelordner
|
# Variable DEPLOY_TARGET aus der git-ignorierten Datei .env im Repo-Wurzelordner
|
||||||
@@ -38,11 +43,13 @@ set -euo pipefail
|
|||||||
# ---- Argumente ----
|
# ---- Argumente ----
|
||||||
YES=0
|
YES=0
|
||||||
PROMOTE=1
|
PROMOTE=1
|
||||||
|
DRY=0
|
||||||
TARGET=""
|
TARGET=""
|
||||||
for arg in "$@"; do
|
for arg in "$@"; do
|
||||||
case "$arg" in
|
case "$arg" in
|
||||||
-y|--yes) YES=1 ;;
|
-y|--yes) YES=1 ;;
|
||||||
--no-promote) PROMOTE=0 ;;
|
--no-promote) PROMOTE=0 ;;
|
||||||
|
--dry-run) DRY=1; PROMOTE=0 ;;
|
||||||
-h|--help)
|
-h|--help)
|
||||||
awk 'NR>2 { if ($0 ~ /^#/) { sub(/^# ?/, ""); print } else exit }' "$0"
|
awk 'NR>2 { if ($0 ~ /^#/) { sub(/^# ?/, ""); print } else exit }' "$0"
|
||||||
exit 0 ;;
|
exit 0 ;;
|
||||||
@@ -158,6 +165,12 @@ sed -e "s/__BACKEND_PORT__/${BACKEND_PORT}/g" scripts/prod.htaccess > "$STAGE/.h
|
|||||||
# abräumen. Bedient Hostsharing die Challenge außerhalb des Docroots, ist es ein
|
# abräumen. Bedient Hostsharing die Challenge außerhalb des Docroots, ist es ein
|
||||||
# No-op.
|
# No-op.
|
||||||
RSYNC_OPTS=(-avz --delete --chmod=D755,F644 --filter='protect /.well-known/***')
|
RSYNC_OPTS=(-avz --delete --chmod=D755,F644 --filter='protect /.well-known/***')
|
||||||
|
if [ "$DRY" -eq 1 ]; then
|
||||||
|
echo "==> Probelauf (rsync --dry-run --delete) nach ${TARGET}:"
|
||||||
|
rsync "${RSYNC_OPTS[@]}" --dry-run "$STAGE"/ "$TARGET"
|
||||||
|
echo "==> Nichts geschrieben (--dry-run)."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
if [ "$YES" -ne 1 ]; then
|
if [ "$YES" -ne 1 ]; then
|
||||||
echo "==> Vorschau (rsync --dry-run --delete) nach ${TARGET}:"
|
echo "==> Vorschau (rsync --dry-run --delete) nach ${TARGET}:"
|
||||||
rsync "${RSYNC_OPTS[@]}" --dry-run "$STAGE"/ "$TARGET"
|
rsync "${RSYNC_OPTS[@]}" --dry-run "$STAGE"/ "$TARGET"
|
||||||
|
|||||||
Executable
+354
@@ -0,0 +1,354 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
#
|
||||||
|
# Werkbaum — ein Befehl für alles, was auf dem Server passiert.
|
||||||
|
#
|
||||||
|
# Verwendung:
|
||||||
|
# remote <ziel> <aktion> [ARGS]
|
||||||
|
#
|
||||||
|
# Backend (systemd-User-Unit werkbaum-backend, Vorgabe ~/opt/werkbaum):
|
||||||
|
# remote backend deploy [-y] [--no-build] [--no-restart]
|
||||||
|
# bauen, hochladen, Unit schreiben, neu starten
|
||||||
|
# und nachsehen, ob /api/v1/info antwortet
|
||||||
|
# remote backend upload [-y] [--no-build]
|
||||||
|
# wie deploy, aber ohne Neustart — das Jar wird
|
||||||
|
# erst beim nächsten Start aktiv
|
||||||
|
# remote backend start|stop|restart|status|enable|disable
|
||||||
|
# den Dienst schalten (systemctl --user)
|
||||||
|
# remote backend log [ARGS] backend.log folgen; weitere ARGS gehen an tail
|
||||||
|
# remote backend info welcher Stand läuft — GET /api/v1/info, wenn
|
||||||
|
# möglich über die öffentliche Adresse, also
|
||||||
|
# samt Proxy-Regel
|
||||||
|
# remote backend setup nur die systemd-Unit neu schreiben und laden
|
||||||
|
# remote backend install-jdk [--force]
|
||||||
|
# JDK 21 ins Home legen oder erneuern
|
||||||
|
# remote backend reset-password [--from-env] [--no-restart]
|
||||||
|
# Master-Passwort der Dokumentenliste setzen
|
||||||
|
# remote backend documents [--from-env]
|
||||||
|
# die Dokumentenliste abrufen — die eine Sache,
|
||||||
|
# für die es das Master-Passwort gibt
|
||||||
|
# remote backend backup [datei] Datenbank sichern. H2 hält die Datei offen,
|
||||||
|
# also: Dienst anhalten, data/ holen, wieder
|
||||||
|
# starten. Vorgabe backups/werkbaum-data-<zeit>.tgz
|
||||||
|
#
|
||||||
|
# Frontend (statische Site, Vorgabe ~/doms/<domain>/htdocs-ssl):
|
||||||
|
# remote frontend deploy [-y] [--no-promote]
|
||||||
|
# befördern (D30), bauen, zusammenstellen,
|
||||||
|
# spiegeln
|
||||||
|
# remote frontend preview nur zeigen, was sich am Ziel änderte —
|
||||||
|
# ohne Beförderung und ohne zu schreiben
|
||||||
|
# remote frontend info welche Version dort liegt (Footer der
|
||||||
|
# ausgelieferten index.html)
|
||||||
|
#
|
||||||
|
# Host:
|
||||||
|
# remote ssh [CMD] Shell auf dem Server, oder CMD dort ausführen
|
||||||
|
#
|
||||||
|
# Konfiguration steht in der git-ignorierten `.env` im Repo-Wurzelordner
|
||||||
|
# (Vorlage: .env.example) — BACKEND_SSH, BACKEND_DIR, BACKEND_PORT und
|
||||||
|
# DEPLOY_TARGET. Anders als im Vorbild gibt es hier nur EIN Konto: derselbe
|
||||||
|
# Login trägt die Site und den Dienst.
|
||||||
|
#
|
||||||
|
# Die eigentliche Arbeit machen weiterhin die Skripte unter `scripts/`; die sind
|
||||||
|
# einzeln aufrufbar und in beiden READMEs beschrieben. Dieses Werkzeug ist die
|
||||||
|
# Vordertür und bringt nur mit, wofür es bisher kein Skript gab: die
|
||||||
|
# systemd-Verben, das Log, die Zustandsabfragen und die Sicherung.
|
||||||
|
#
|
||||||
|
# Siehe README (Abschnitt Deployment) und docs/DECISIONS.md D77.
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||||
|
cd "$ROOT"
|
||||||
|
# shellcheck source=../scripts/lib-env.sh
|
||||||
|
. "$ROOT/scripts/lib-env.sh"
|
||||||
|
|
||||||
|
UNIT=werkbaum-backend.service
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
awk 'NR>2 { if ($0 ~ /^#/) { sub(/^# ?/, ""); print } else exit }' "$0"
|
||||||
|
}
|
||||||
|
|
||||||
|
die() { echo "$*" >&2; exit 1; }
|
||||||
|
|
||||||
|
# Das SSH-Ziel: BACKEND_SSH, sonst der user@host-Teil von DEPLOY_TARGET. Beides
|
||||||
|
# ist derselbe Login, aber DEPLOY_TARGET trägt zusätzlich einen Pfad — den
|
||||||
|
# schneidet das `%%:*` weg.
|
||||||
|
sshTarget() {
|
||||||
|
local t; t="$(env_value BACKEND_SSH)"
|
||||||
|
if [ -z "$t" ]; then
|
||||||
|
t="$(env_value DEPLOY_TARGET)"; t="${t%%:*}"
|
||||||
|
fi
|
||||||
|
[ -n "$t" ] || die "Kein SSH-Ziel: BACKEND_SSH (oder DEPLOY_TARGET) in .env setzen."
|
||||||
|
printf '%s' "$t"
|
||||||
|
}
|
||||||
|
|
||||||
|
backendDir() { local d; d="$(env_value BACKEND_DIR)"; printf '%s' "${d:-opt/werkbaum}"; }
|
||||||
|
backendPort() { local p; p="$(env_value BACKEND_PORT)"; printf '%s' "${p:-9080}"; }
|
||||||
|
|
||||||
|
# Derselbe Pfad in der Schreibweise, die eine Shell auf dem Server versteht.
|
||||||
|
# rsync und systemd brauchen andere — die kennt scripts/deploy-backend.sh.
|
||||||
|
backendDirSh() {
|
||||||
|
local d; d="$(backendDir)"
|
||||||
|
case "$d" in /*) printf '%s' "$d" ;; *) printf '$HOME/%s' "$d" ;; esac
|
||||||
|
}
|
||||||
|
|
||||||
|
# Die öffentliche Adresse steht nirgends ausdrücklich, wohl aber im rsync-Ziel
|
||||||
|
# des Frontends (`.../doms/<domain>/htdocs-ssl`). Lässt sie sich nicht ablesen,
|
||||||
|
# geben die Aufrufer eine ehrliche Antwort statt zu raten.
|
||||||
|
publicBase() {
|
||||||
|
env_value DEPLOY_TARGET | sed -n 's|.*/doms/\([^/]*\)/.*|https://\1|p'
|
||||||
|
}
|
||||||
|
|
||||||
|
# `systemctl --user` findet seinen Manager ohne XDG_RUNTIME_DIR nicht — über
|
||||||
|
# eine nicht-interaktive SSH-Sitzung ist die Variable oft nicht gesetzt. Die
|
||||||
|
# klassische Falle bei User-Units aus einem Skript heraus (D77).
|
||||||
|
sshSystemctl() {
|
||||||
|
ssh "$(sshTarget)" \
|
||||||
|
"export XDG_RUNTIME_DIR=\${XDG_RUNTIME_DIR:-/run/user/\$(id -u)}; systemctl --user $*"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Ein tty nur, wo es eins gibt — sonst warnt ssh einen skriptenden Aufrufer an.
|
||||||
|
sshHost() {
|
||||||
|
local ttyOpt=-t; [ -t 0 ] || ttyOpt=
|
||||||
|
# shellcheck disable=SC2086
|
||||||
|
exec ssh $ttyOpt "$(sshTarget)" "$@"
|
||||||
|
}
|
||||||
|
|
||||||
|
badAction() {
|
||||||
|
if [ -z "$2" ]; then echo "Ziel '$1' braucht eine Aktion." >&2
|
||||||
|
else echo "Unbekannte Aktion '$2' für Ziel '$1'." >&2; fi
|
||||||
|
usage >&2
|
||||||
|
exit 2
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------- backend ----
|
||||||
|
|
||||||
|
cmdBackendLog() {
|
||||||
|
# tty nur, wo es eins gibt — sonst warnt ssh und die Warnung landet mitten im
|
||||||
|
# Log. Gemessen: „Pseudo-terminal will not be allocated ...“ als erste Zeile.
|
||||||
|
local ttyOpt=-t; [ -t 0 ] || ttyOpt=
|
||||||
|
# shellcheck disable=SC2086
|
||||||
|
exec ssh $ttyOpt "$(sshTarget)" "tail -f $* \"$(backendDirSh)/backend.log\""
|
||||||
|
}
|
||||||
|
|
||||||
|
# Wartet, bis der Dienst wieder antwortet. Direkt an ihm gefragt (127.0.0.1),
|
||||||
|
# nicht über den Proxy: Hier geht es darum, ob er oben ist — am Proxy hat sich
|
||||||
|
# nichts geändert. Ohne dieses Warten sagt ein Befehl „Dienst wieder gestartet",
|
||||||
|
# während die nächste Anfrage noch 503 bekommt (beim Bauen gemessen: ~14 s).
|
||||||
|
waitForBackend() {
|
||||||
|
ssh "$(sshTarget)" "
|
||||||
|
export XDG_RUNTIME_DIR=\${XDG_RUNTIME_DIR:-/run/user/\$(id -u)}
|
||||||
|
systemctl --user is-active --quiet $UNIT || { echo ' Dienst läuft nicht.'; exit 0; }
|
||||||
|
for i in \$(seq 1 60); do
|
||||||
|
case \"\$(curl -s --max-time 3 http://127.0.0.1:$(backendPort)/api/v1/info || true)\" in
|
||||||
|
*'\"version\"'*) echo \" Antwortet wieder (nach \${i}s).\"; exit 0 ;;
|
||||||
|
esac
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
echo ' ! Antwortet nicht — nachsehen mit: remote backend log' >&2; exit 1"
|
||||||
|
}
|
||||||
|
|
||||||
|
cmdBackendInfo() {
|
||||||
|
local base; base="$(publicBase)"
|
||||||
|
if [ -n "$base" ]; then
|
||||||
|
echo "==> ${base}/api/v1/info"
|
||||||
|
# --fail, damit ein 502 des Proxys nicht als Erfolg mit leerem Rumpf endet.
|
||||||
|
curl -sS --fail --max-time 10 "${base}/api/v1/info" || {
|
||||||
|
echo >&2
|
||||||
|
echo " Antwortet nicht über die öffentliche Adresse. Läuft der Dienst?" >&2
|
||||||
|
echo " Nachsehen: remote backend status · remote backend log" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
echo
|
||||||
|
else
|
||||||
|
# Ohne ablesbare Domain wenigstens direkt am Dienst fragen. Das prüft dann
|
||||||
|
# den Proxy NICHT mit — deshalb wird es auch so gesagt.
|
||||||
|
echo "==> keine Domain aus DEPLOY_TARGET ablesbar — frage direkt am Dienst"
|
||||||
|
ssh "$(sshTarget)" "curl -sS --fail --max-time 10 http://127.0.0.1:$(backendPort)/api/v1/info"
|
||||||
|
echo
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Die Dokumentenliste ist der einzige geschützte Endpunkt (D76-Nachtrag 6).
|
||||||
|
# Ohne --from-env fragt curl selbst nach dem Passwort; mit --from-env kommt es
|
||||||
|
# aus der .env — in beiden Fällen NIE über die Kommandozeile: `curl -u user:pw`
|
||||||
|
# stünde in der Prozessliste, die auf einem geteilten Host jeder lesen kann,
|
||||||
|
# und die Shell fasst das Passwort vorher an (aus `ge$heim` wird `ge`).
|
||||||
|
cmdBackendDocuments() {
|
||||||
|
local fromEnv=0
|
||||||
|
for a in "$@"; do
|
||||||
|
case "$a" in
|
||||||
|
--from-env) fromEnv=1 ;;
|
||||||
|
*) die "Unbekannte Option: $a" ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
local base; base="$(publicBase)"
|
||||||
|
[ -n "$base" ] || die "Keine Domain aus DEPLOY_TARGET ablesbar — Adresse unbekannt."
|
||||||
|
local url="${base}/api/v1/documents"
|
||||||
|
|
||||||
|
if [ "$fromEnv" -eq 1 ]; then
|
||||||
|
local pw; pw="$(env_value WERKBAUM_MASTER_PASSWORT)"
|
||||||
|
[ -n "$pw" ] || die "WERKBAUM_MASTER_PASSWORT steht nicht in ${ENV_FILE}."
|
||||||
|
# curl liest die Konfiguration von stdin; in doppelten Anführungszeichen
|
||||||
|
# sind dort `\` und `"` zu escapen, sonst nichts.
|
||||||
|
local esc=${pw//\\/\\\\}; esc=${esc//\"/\\\"}
|
||||||
|
echo "==> ${url} (Passwort aus ${ENV_FILE})"
|
||||||
|
printf 'user = "werkbaum:%s"\n' "$esc" | curl -sS --fail -K - "$url" || {
|
||||||
|
echo >&2; echo " 401? Dann passt der Hash auf dem Server nicht zum" >&2
|
||||||
|
echo " Passwort in der .env: remote backend reset-password --from-env" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
else
|
||||||
|
echo "==> ${url}"
|
||||||
|
curl -sS --fail -u werkbaum "$url" || { echo >&2; exit 1; }
|
||||||
|
fi
|
||||||
|
echo
|
||||||
|
}
|
||||||
|
|
||||||
|
# Sichern heißt hier: anhalten, holen, wieder starten. H2 hält die Datei offen,
|
||||||
|
# solange der Dienst läuft — eine Kopie im Betrieb kann zerrissen sein, und eine
|
||||||
|
# Sicherung, der man nicht trauen kann, ist keine. Die Auszeit dauert Sekunden.
|
||||||
|
cmdBackendBackup() {
|
||||||
|
local out="${1:-}"
|
||||||
|
if [ -z "$out" ]; then
|
||||||
|
mkdir -p "$ROOT/backups"
|
||||||
|
out="$ROOT/backups/werkbaum-data-$(date +%Y%m%d-%H%M%S).tgz"
|
||||||
|
fi
|
||||||
|
[ ! -e "$out" ] || die "Gibt es schon: $out"
|
||||||
|
|
||||||
|
echo "==> Sichern von $(sshTarget):$(backendDir)/data nach ${out}"
|
||||||
|
echo " (der Dienst hält dafür kurz an)"
|
||||||
|
|
||||||
|
# Der Tar-Strom geht über stdout in die Datei — ALLE Meldungen müssen deshalb
|
||||||
|
# nach stderr, sonst landen sie im Archiv. Und der Trap muss auch dann noch
|
||||||
|
# starten, wenn die lokale Seite wegbricht: Ohne PIPE bliebe der Dienst
|
||||||
|
# ausgerechnet dann unten, wenn beim Herunterladen etwas schiefgeht.
|
||||||
|
local tmp="${out}.teil"
|
||||||
|
if ! ssh "$(sshTarget)" DIR="$(backendDirSh)" UNIT="$UNIT" 'bash -s' > "$tmp" <<'REMOTE'
|
||||||
|
set -euo pipefail
|
||||||
|
DIR="$(eval echo "$DIR")"
|
||||||
|
export XDG_RUNTIME_DIR="${XDG_RUNTIME_DIR:-/run/user/$(id -u)}"
|
||||||
|
|
||||||
|
[ -d "$DIR/data" ] || { echo "Kein $DIR/data auf dem Server." >&2; exit 1; }
|
||||||
|
|
||||||
|
LIEF=0
|
||||||
|
systemctl --user is-active --quiet "$UNIT" && LIEF=1
|
||||||
|
wiederAn() {
|
||||||
|
if [ "$LIEF" = 1 ]; then
|
||||||
|
# NICHTS nach stdout: dort liegt der Tar-Strom. Ein einziges Wort darin
|
||||||
|
# macht das Archiv unlesbar, und der Trap kann an jeder Stelle zuschlagen.
|
||||||
|
systemctl --user start "$UNIT" >/dev/null 2>&1 || true
|
||||||
|
echo " Dienst wieder gestartet." >&2
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
trap wiederAn EXIT HUP INT TERM PIPE
|
||||||
|
|
||||||
|
if [ "$LIEF" = 1 ]; then
|
||||||
|
echo " Dienst anhalten ..." >&2
|
||||||
|
systemctl --user stop "$UNIT" >&2
|
||||||
|
else
|
||||||
|
echo " Dienst lief nicht — nichts anzuhalten." >&2
|
||||||
|
fi
|
||||||
|
|
||||||
|
tar czf - -C "$DIR" data
|
||||||
|
REMOTE
|
||||||
|
then
|
||||||
|
rm -f "$tmp"
|
||||||
|
die "Sicherung fehlgeschlagen — nichts geschrieben."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Gegenprobe, bevor jemand der Datei vertraut: Lässt sie sich lesen, und ist
|
||||||
|
# die Datenbank drin? Ein Archiv, das man nie geöffnet hat, ist eine Hoffnung.
|
||||||
|
if ! tar tzf "$tmp" >/dev/null 2>&1; then
|
||||||
|
rm -f "$tmp"
|
||||||
|
die "Das Archiv ist nicht lesbar — nichts geschrieben."
|
||||||
|
fi
|
||||||
|
if ! tar tzf "$tmp" | grep -q 'data/editor.*\.db'; then
|
||||||
|
rm -f "$tmp"
|
||||||
|
die "Im Archiv liegt keine H2-Datenbank (data/editor*.db) — nichts geschrieben."
|
||||||
|
fi
|
||||||
|
mv "$tmp" "$out"
|
||||||
|
|
||||||
|
echo "==> $(du -h "$out" | cut -f1) in ${out}"
|
||||||
|
echo " Inhalt: $(tar tzf "$out" | tr '\n' ' ')"
|
||||||
|
|
||||||
|
waitForBackend
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo " Zurückspielen — der Dienst muss dafür stehen:"
|
||||||
|
echo " remote backend stop"
|
||||||
|
echo " ssh $(sshTarget) 'rm -rf $(backendDirSh)/data'"
|
||||||
|
echo " ssh $(sshTarget) 'tar xzf - -C $(backendDirSh)' < ${out}"
|
||||||
|
echo " remote backend start"
|
||||||
|
}
|
||||||
|
|
||||||
|
cmdBackend() {
|
||||||
|
local action="${1:-}"; shift || true
|
||||||
|
case "$action" in
|
||||||
|
deploy) scripts/deploy-backend.sh "$@" ;;
|
||||||
|
upload) scripts/deploy-backend.sh --no-restart "$@" ;;
|
||||||
|
setup) scripts/deploy-backend.sh --unit-only "$@" ;;
|
||||||
|
install-jdk) scripts/install-jdk.sh "$@" ;;
|
||||||
|
reset-password) scripts/reset-password.sh "$@" ;;
|
||||||
|
log) cmdBackendLog "$@" ;;
|
||||||
|
info) cmdBackendInfo "$@" ;;
|
||||||
|
documents) cmdBackendDocuments "$@" ;;
|
||||||
|
backup) cmdBackendBackup "$@" ;;
|
||||||
|
status) sshSystemctl "status $UNIT --no-pager -l" ;;
|
||||||
|
enable|disable) sshSystemctl "$action --now $UNIT" ;;
|
||||||
|
start|stop|restart) sshSystemctl "$action $UNIT" ;;
|
||||||
|
*) badAction backend "$action" ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
# --------------------------------------------------------------- frontend ----
|
||||||
|
|
||||||
|
# Was liegt am Ziel? Der Footer trägt Version und den Commit, aus dem gebaut
|
||||||
|
# wurde (D16) — die einzige Stelle, an der die ausgelieferte Datei sagt, was
|
||||||
|
# sie ist. Deshalb wird sie gelesen und nicht der lokale Stand behauptet.
|
||||||
|
cmdFrontendInfo() {
|
||||||
|
local base; base="$(publicBase)"
|
||||||
|
[ -n "$base" ] || die "Keine Domain aus DEPLOY_TARGET ablesbar."
|
||||||
|
echo "==> ${base}/"
|
||||||
|
local html
|
||||||
|
html="$(curl -sS --fail --max-time 15 "${base}/")" \
|
||||||
|
|| die " Die Seite antwortet nicht."
|
||||||
|
local ver commit
|
||||||
|
ver="$(printf '%s' "$html" | grep -o '<a class="ver"[^>]*>[^<]*</a>' | head -1)"
|
||||||
|
commit="$(printf '%s' "$ver" | sed -n 's/.*href="\([^"]*\)".*/\1/p')"
|
||||||
|
if [ -z "$ver" ]; then
|
||||||
|
echo " Kein Versions-Link im Footer — ist das wirklich Werkbaum?"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo " Version: $(printf '%s' "$ver" | sed -n 's/.*>\([^<]*\)<.*/\1/p')"
|
||||||
|
echo " Commit: ${commit}"
|
||||||
|
# Ob der ausgelieferte Stand der eigene ist, sagt nur der Vergleich.
|
||||||
|
local head; head="$(git -C "$ROOT" rev-parse HEAD 2>/dev/null || true)"
|
||||||
|
if [ -n "$head" ]; then
|
||||||
|
case "$commit" in
|
||||||
|
*"$head") echo " Das ist genau dein HEAD." ;;
|
||||||
|
*) echo " Dein HEAD ist ein anderer: ${head}" ;;
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
cmdFrontend() {
|
||||||
|
local action="${1:-}"; shift || true
|
||||||
|
case "$action" in
|
||||||
|
deploy) scripts/deploy-prod.sh "$@" ;;
|
||||||
|
preview) scripts/deploy-prod.sh --dry-run "$@" ;;
|
||||||
|
info) cmdFrontendInfo "$@" ;;
|
||||||
|
*) badAction frontend "$action" ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------ Start ----
|
||||||
|
|
||||||
|
target="${1:-}"; shift || true
|
||||||
|
case "$target" in
|
||||||
|
backend) cmdBackend "$@" ;;
|
||||||
|
frontend) cmdFrontend "$@" ;;
|
||||||
|
ssh) sshHost "$@" ;;
|
||||||
|
''|-h|--help) usage ;;
|
||||||
|
*) echo "Unbekanntes Ziel '$target'." >&2; usage >&2; exit 2 ;;
|
||||||
|
esac
|
||||||
Reference in New Issue
Block a user