feat: add Docker/OpenTofu deployment and DE/NL routing
This commit is contained in:
@@ -0,0 +1,31 @@
|
||||
# Diese Datei nach deploy/.env.production kopieren.
|
||||
# Sie ist zugleich eine POSIX-Shell- und Docker-Compose-kompatible Env-Datei.
|
||||
|
||||
WATERMAPS_DOMAIN=watermaps.incoso.eu
|
||||
WATERMAPS_ACME_EMAIL=REPLACE_WITH_REAL_EMAIL
|
||||
|
||||
# Wird von go-live.sh geprüft. upload-and-deploy.sh übergibt die von OpenTofu
|
||||
# ausgegebene IPv4 automatisch; bei direkter Serverausführung hier eintragen.
|
||||
WATERMAPS_EXPECTED_IPV4=
|
||||
|
||||
# Dauerhafte Routing-Rohdaten und der erzeugte Routingindex.
|
||||
WATERMAPS_DATA_DIR=/srv/watermaps-data
|
||||
|
||||
# Generierte Nginx-Konfiguration und lokale Sperrdateien.
|
||||
WATERMAPS_RUNTIME_DIR=/srv/watermaps-runtime
|
||||
|
||||
# Verhindert Speicherabbrüche beim vollständigen DE/NL-Indexaufbau auf 4-GB-Servern.
|
||||
WATERMAPS_SWAP_SIZE_GB=4
|
||||
|
||||
WATERMAPS_APP_IMAGE=watermaps:production
|
||||
WATERMAPS_NGINX_IMAGE=nginx:1.30.4-alpine
|
||||
WATERMAPS_CERTBOT_IMAGE=certbot/certbot:v5.7.0
|
||||
WATERMAPS_ROUTE_DATA_IMAGE=watermaps-route-data:production
|
||||
|
||||
# Nur auf true setzen, wenn bei jedem Deployment Deutschland und die
|
||||
# Niederlande erneut geprüft und der Routingindex neu gebaut werden sollen.
|
||||
WATERMAPS_REBUILD_ROUTE_DATA=false
|
||||
|
||||
# Für einen Test gegen Let's Encrypt Staging auf true setzen.
|
||||
# Das damit ausgestellte Zertifikat ist im Browser nicht vertrauenswürdig.
|
||||
WATERMAPS_CERTBOT_STAGING=false
|
||||
@@ -0,0 +1,2 @@
|
||||
.env.production
|
||||
!.env.production.example
|
||||
@@ -0,0 +1,107 @@
|
||||
# Watermaps-Produktion
|
||||
|
||||
Dieser Stack hostet die Watermaps-App und die lokalen Fahrrouten für
|
||||
Deutschland und die Niederlande. Die sichtbaren Kartenkacheln bleiben externe
|
||||
Dienste. Öffentlich gebunden werden ausschließlich TCP 80 und 443; die
|
||||
Watermaps-App ist nur im internen Docker-Netz erreichbar.
|
||||
|
||||
## Konfiguration
|
||||
|
||||
```bash
|
||||
cp deploy/.env.production.example deploy/.env.production
|
||||
editor deploy/.env.production
|
||||
```
|
||||
|
||||
Mindestens `WATERMAPS_ACME_EMAIL` muss angepasst werden. Der Hetzner-API-Token
|
||||
gehört **nicht** in diese Datei. Er bleibt lokal in der ignorierten Datei
|
||||
`infra/opentofu/terraform.tfvars` (alternativ kann der Provider
|
||||
`TF_VAR_hcloud_token` lesen). Der Upload schließt Terraform-Variablen, Pläne,
|
||||
State, lokale Env-Dateien und `.terraform` in jeder Verzeichnistiefe aus.
|
||||
|
||||
Die private SSH-Keydatei wird ebenfalls nicht gespeichert. Sie kann beim
|
||||
Deployment mit `--identity` oder über `WATERMAPS_SSH_KEY` angegeben werden.
|
||||
Falls OpenTofu keinen Output `ssh_private_key_path` bereitstellt und der Key
|
||||
nicht bereits über den SSH-Agenten verfügbar ist, ist eine dieser beiden
|
||||
Angaben erforderlich. Der vorbereitete SSH-Benutzer heißt standardmäßig
|
||||
`deploy`; die privilegierten Installationsschritte laufen über dessen
|
||||
passwortloses `sudo`.
|
||||
|
||||
## Deployment
|
||||
|
||||
Nach `tofu apply` liest dieses Skript standardmäßig den Output `server_ipv4`,
|
||||
überträgt das Projekt ohne State, lokale Daten oder Node-Module und startet den
|
||||
Serverbootstrap sowie den Docker-Stack:
|
||||
|
||||
```bash
|
||||
./deploy/scripts/upload-and-deploy.sh --identity ~/.ssh/watermaps_hetzner_ed25519
|
||||
```
|
||||
|
||||
Alternativ:
|
||||
|
||||
```bash
|
||||
WATERMAPS_SERVER_IPV4=203.0.113.10 \
|
||||
WATERMAPS_SSH_KEY=~/.ssh/watermaps_hetzner_ed25519 \
|
||||
./deploy/scripts/upload-and-deploy.sh
|
||||
```
|
||||
|
||||
Der erste Datenaufbau lädt die Geofabrik-Extrakte für Deutschland und die
|
||||
Niederlande herunter und kann entsprechend der Serverleistung längere Zeit
|
||||
dauern. Der produktive Index liegt auf dem Server unter:
|
||||
|
||||
```text
|
||||
/srv/watermaps-data/local/germany-netherlands-fairways.json
|
||||
```
|
||||
|
||||
Der Upload wartet zuerst auf SSH und den Abschluss von Cloud-init. Anschließend
|
||||
startet und prüft er `watermaps-volume-setup.service`. Ohne tatsächlich unter
|
||||
`/srv/watermaps-data` eingehängtes Volume wird kein Download gestartet, damit
|
||||
die großen PBF-Dateien nicht versehentlich auf dem Root-Dateisystem landen.
|
||||
Für den kurzzeitigen Speicherpeak beim kombinierten Indexaufbau richtet der
|
||||
Bootstrap zusätzlich die über `WATERMAPS_SWAP_SIZE_GB` konfigurierte,
|
||||
persistente Swap-Reserve ein.
|
||||
|
||||
Vor dem Livegang antwortet Nginx nur für ACME-Challenges. Alle anderen
|
||||
HTTP-Anfragen erhalten 404.
|
||||
|
||||
## DNS und SSL-Livegang
|
||||
|
||||
Sobald der OpenTofu-Output bekannt ist, kann folgender DNS-Eintrag gesetzt
|
||||
werden:
|
||||
|
||||
```text
|
||||
A watermaps.incoso.eu <server_ipv4>
|
||||
```
|
||||
|
||||
Nach der DNS-Propagation wird der Livegang lokal ausgelöst:
|
||||
|
||||
```bash
|
||||
./deploy/scripts/remote-go-live.sh --identity ~/.ssh/watermaps_hetzner_ed25519
|
||||
```
|
||||
|
||||
Das Serverskript prüft, dass sämtliche A-Records ausschließlich auf die
|
||||
erwartete IPv4 zeigen, testet den Routingindex mit je einer Route in
|
||||
Deutschland und den Niederlanden, prüft den ACME-Webroot, fordert das
|
||||
Zertifikat an und aktiviert erst anschließend HTTPS. HTTP leitet danach auf
|
||||
HTTPS um.
|
||||
|
||||
## Automatik
|
||||
|
||||
`bootstrap-server.sh` installiert zwei systemd-Timer:
|
||||
|
||||
- `watermaps-route-update.timer`: täglich neue Deutschland- und
|
||||
Niederlande-Daten; bei Build- oder Routentestfehler bleibt der vorherige
|
||||
Index aktiv.
|
||||
- `watermaps-certbot-renew.timer`: zweimal täglich Certbot-Prüfung mit
|
||||
anschließendem Nginx-Reload.
|
||||
|
||||
Status und Logs:
|
||||
|
||||
```bash
|
||||
systemctl list-timers 'watermaps-*'
|
||||
journalctl -u watermaps-route-update.service
|
||||
journalctl -u watermaps-certbot-renew.service
|
||||
docker compose \
|
||||
--project-directory /opt/watermaps \
|
||||
--env-file /opt/watermaps/deploy/.env.production \
|
||||
-f /opt/watermaps/deploy/compose.production.yml ps
|
||||
```
|
||||
@@ -0,0 +1,100 @@
|
||||
name: watermaps-production
|
||||
|
||||
services:
|
||||
watermaps:
|
||||
image: ${WATERMAPS_APP_IMAGE:-watermaps:production}
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
environment:
|
||||
NODE_ENV: production
|
||||
HOST: 0.0.0.0
|
||||
PORT: 5174
|
||||
DATABASE_URL: ""
|
||||
REDIS_URL: ""
|
||||
WATERMAPS_WEB_DIST_PATH: /app/apps/web/dist
|
||||
WATERMAPS_LOCAL_FAIRWAYS_PATH: /data/germany-netherlands-fairways.json
|
||||
WATERMAPS_LIVE_FAIRWAYS: "false"
|
||||
WATERMAPS_DEMO_DATA: "false"
|
||||
expose:
|
||||
- "5174"
|
||||
volumes:
|
||||
- type: bind
|
||||
source: ${WATERMAPS_DATA_DIR:-/srv/watermaps-data}/local
|
||||
target: /data
|
||||
read_only: true
|
||||
restart: unless-stopped
|
||||
init: true
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
cap_drop:
|
||||
- ALL
|
||||
healthcheck:
|
||||
test:
|
||||
- CMD
|
||||
- node
|
||||
- -e
|
||||
- >-
|
||||
fetch('http://127.0.0.1:5174/health')
|
||||
.then(response => { if (!response.ok) process.exit(1) })
|
||||
.catch(() => process.exit(1))
|
||||
interval: 15s
|
||||
timeout: 5s
|
||||
start_period: 15s
|
||||
retries: 5
|
||||
|
||||
nginx:
|
||||
image: ${WATERMAPS_NGINX_IMAGE:-nginx:1.30.4-alpine}
|
||||
depends_on:
|
||||
watermaps:
|
||||
condition: service_healthy
|
||||
ports:
|
||||
- "80:80"
|
||||
- "443:443"
|
||||
volumes:
|
||||
- type: bind
|
||||
source: ${WATERMAPS_RUNTIME_DIR:-/srv/watermaps-runtime}/nginx/conf.d
|
||||
target: /etc/nginx/conf.d
|
||||
read_only: true
|
||||
- type: bind
|
||||
source: ${WATERMAPS_DATA_DIR:-/srv/watermaps-data}/certbot/www
|
||||
target: /var/www/certbot
|
||||
read_only: true
|
||||
- type: bind
|
||||
source: ${WATERMAPS_DATA_DIR:-/srv/watermaps-data}/certbot/letsencrypt
|
||||
target: /etc/letsencrypt
|
||||
read_only: true
|
||||
restart: unless-stopped
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
healthcheck:
|
||||
test: ["CMD", "nginx", "-t"]
|
||||
interval: 30s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
|
||||
certbot:
|
||||
image: ${WATERMAPS_CERTBOT_IMAGE:-certbot/certbot:v5.7.0}
|
||||
profiles: ["maintenance"]
|
||||
volumes:
|
||||
- type: bind
|
||||
source: ${WATERMAPS_DATA_DIR:-/srv/watermaps-data}/certbot/www
|
||||
target: /var/www/certbot
|
||||
- type: bind
|
||||
source: ${WATERMAPS_DATA_DIR:-/srv/watermaps-data}/certbot/letsencrypt
|
||||
target: /etc/letsencrypt
|
||||
|
||||
route-data:
|
||||
image: ${WATERMAPS_ROUTE_DATA_IMAGE:-watermaps-route-data:production}
|
||||
build:
|
||||
context: .
|
||||
dockerfile: deploy/route-data.Dockerfile
|
||||
profiles: ["maintenance"]
|
||||
environment:
|
||||
WATERMAPS_GEOFABRIK_DIR: /workspace/data/geofabrik
|
||||
WATERMAPS_LOCAL_FAIRWAYS_PATH: /workspace/data/local/germany-netherlands-fairways.json
|
||||
volumes:
|
||||
- type: bind
|
||||
source: ${WATERMAPS_DATA_DIR:-/srv/watermaps-data}
|
||||
target: /workspace/data
|
||||
restart: "no"
|
||||
@@ -0,0 +1,15 @@
|
||||
server {
|
||||
listen 80 default_server;
|
||||
listen [::]:80 default_server;
|
||||
server_name _;
|
||||
|
||||
location ^~ /.well-known/acme-challenge/ {
|
||||
root /var/www/certbot;
|
||||
default_type text/plain;
|
||||
try_files $uri =404;
|
||||
}
|
||||
|
||||
location / {
|
||||
return 404;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
server {
|
||||
listen 80 default_server;
|
||||
listen [::]:80 default_server;
|
||||
server_name _;
|
||||
|
||||
location ^~ /.well-known/acme-challenge/ {
|
||||
root /var/www/certbot;
|
||||
default_type text/plain;
|
||||
try_files $uri =404;
|
||||
}
|
||||
|
||||
location / {
|
||||
return 404;
|
||||
}
|
||||
}
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
listen [::]:80;
|
||||
server_name __WATERMAPS_DOMAIN__;
|
||||
|
||||
location ^~ /.well-known/acme-challenge/ {
|
||||
root /var/www/certbot;
|
||||
default_type text/plain;
|
||||
try_files $uri =404;
|
||||
}
|
||||
|
||||
location / {
|
||||
return 308 https://__WATERMAPS_DOMAIN__$request_uri;
|
||||
}
|
||||
}
|
||||
|
||||
server {
|
||||
listen 443 ssl;
|
||||
listen [::]:443 ssl;
|
||||
http2 on;
|
||||
server_name __WATERMAPS_DOMAIN__;
|
||||
|
||||
ssl_certificate /etc/letsencrypt/live/__WATERMAPS_CERT_NAME__/fullchain.pem;
|
||||
ssl_certificate_key /etc/letsencrypt/live/__WATERMAPS_CERT_NAME__/privkey.pem;
|
||||
ssl_protocols TLSv1.2 TLSv1.3;
|
||||
ssl_session_cache shared:SSL:10m;
|
||||
ssl_session_timeout 1d;
|
||||
ssl_session_tickets off;
|
||||
|
||||
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
|
||||
add_header X-Content-Type-Options nosniff always;
|
||||
add_header Referrer-Policy strict-origin-when-cross-origin always;
|
||||
|
||||
client_max_body_size 2m;
|
||||
|
||||
location / {
|
||||
if ($host != "__WATERMAPS_DOMAIN__") {
|
||||
return 404;
|
||||
}
|
||||
|
||||
proxy_pass http://watermaps:5174;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Host $host;
|
||||
proxy_set_header X-Forwarded-Proto https;
|
||||
proxy_read_timeout 60s;
|
||||
proxy_send_timeout 60s;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
FROM python:3.12-slim
|
||||
|
||||
RUN apt-get update \
|
||||
&& apt-get install --yes --no-install-recommends ca-certificates curl coreutils libexpat1 \
|
||||
&& rm -rf /var/lib/apt/lists/* \
|
||||
&& python3 -m pip install --no-cache-dir "osmium==4.3.1"
|
||||
|
||||
WORKDIR /workspace
|
||||
|
||||
COPY scripts ./scripts
|
||||
COPY deploy/scripts/prepare-route-data.sh ./deploy/scripts/prepare-route-data.sh
|
||||
|
||||
ENTRYPOINT ["/workspace/deploy/scripts/prepare-route-data.sh"]
|
||||
Executable
+111
@@ -0,0 +1,111 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -Eeuo pipefail
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
if [[ "$(id -u)" -ne 0 ]]; then
|
||||
printf 'Dieses Skript muss als root ausgeführt werden.\n' >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
WM_DEPLOY_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
WM_ROOT_DIR="$(cd "$WM_DEPLOY_DIR/.." && pwd)"
|
||||
WM_ENV_FILE="${WATERMAPS_ENV_FILE:-$WM_DEPLOY_DIR/.env.production}"
|
||||
|
||||
install_docker() {
|
||||
if command -v docker >/dev/null 2>&1 && docker compose version >/dev/null 2>&1; then
|
||||
return
|
||||
fi
|
||||
|
||||
. /etc/os-release
|
||||
case "${ID:-}" in
|
||||
ubuntu|debian) ;;
|
||||
*)
|
||||
printf 'Nicht unterstützte Distribution für die automatische Docker-Installation: %s\n' "${ID:-unbekannt}" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
apt-get update
|
||||
apt-get install --yes ca-certificates curl gnupg
|
||||
install -m 0755 -d /etc/apt/keyrings
|
||||
curl --fail --silent --show-error --location \
|
||||
"https://download.docker.com/linux/$ID/gpg" \
|
||||
--output /etc/apt/keyrings/docker.asc
|
||||
chmod a+r /etc/apt/keyrings/docker.asc
|
||||
|
||||
architecture="$(dpkg --print-architecture)"
|
||||
codename="${VERSION_CODENAME:?VERSION_CODENAME fehlt in /etc/os-release}"
|
||||
printf 'deb [arch=%s signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/%s %s stable\n' \
|
||||
"$architecture" "$ID" "$codename" \
|
||||
>/etc/apt/sources.list.d/docker.list
|
||||
|
||||
apt-get update
|
||||
apt-get install --yes docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
|
||||
}
|
||||
|
||||
install_docker
|
||||
apt-get update
|
||||
apt-get install --yes bind9-dnsutils curl jq rsync
|
||||
systemctl enable --now docker
|
||||
|
||||
if [[ ! -f "$WM_ENV_FILE" ]]; then
|
||||
install -m 0600 "$WM_DEPLOY_DIR/.env.production.example" "$WM_ENV_FILE"
|
||||
printf 'Konfiguration aus Vorlage angelegt: %s\n' "$WM_ENV_FILE"
|
||||
fi
|
||||
|
||||
# shellcheck source=common.sh
|
||||
source "$WM_DEPLOY_DIR/scripts/common.sh"
|
||||
wm_load_env
|
||||
wm_assert_data_mount
|
||||
|
||||
ensure_swap_reserve() {
|
||||
local swap_file="/swapfile"
|
||||
local swap_size_gb="${WATERMAPS_SWAP_SIZE_GB:-4}"
|
||||
|
||||
[[ "$swap_size_gb" =~ ^[1-9][0-9]*$ ]] &&
|
||||
((swap_size_gb <= 16)) ||
|
||||
wm_die "WATERMAPS_SWAP_SIZE_GB muss eine ganze Zahl zwischen 1 und 16 sein."
|
||||
|
||||
if swapon --show=NAME --noheadings | awk '{$1=$1; print}' | grep -Fxq "$swap_file"; then
|
||||
return
|
||||
fi
|
||||
|
||||
wm_log "Persistente ${swap_size_gb}-GB-Swap-Reserve wird vorbereitet."
|
||||
fallocate --length "${swap_size_gb}G" "$swap_file"
|
||||
chmod 0600 "$swap_file"
|
||||
mkswap --force "$swap_file" >/dev/null
|
||||
if ! grep -Fq "$swap_file none swap sw 0 0" /etc/fstab; then
|
||||
printf '%s\n' "$swap_file none swap sw 0 0" >>/etc/fstab
|
||||
fi
|
||||
swapon "$swap_file"
|
||||
}
|
||||
|
||||
ensure_swap_reserve
|
||||
|
||||
install -d -m 0755 \
|
||||
"$WATERMAPS_DATA_DIR" \
|
||||
"$WATERMAPS_DATA_DIR/geofabrik" \
|
||||
"$WATERMAPS_DATA_DIR/local" \
|
||||
"$WATERMAPS_DATA_DIR/certbot" \
|
||||
"$WATERMAPS_DATA_DIR/certbot/www" \
|
||||
"$WATERMAPS_DATA_DIR/certbot/letsencrypt" \
|
||||
"$WATERMAPS_RUNTIME_DIR" \
|
||||
"$WATERMAPS_RUNTIME_DIR/nginx" \
|
||||
"$WATERMAPS_RUNTIME_DIR/nginx/conf.d" \
|
||||
"$WATERMAPS_RUNTIME_DIR/locks"
|
||||
|
||||
if [[ ! -f "$WATERMAPS_RUNTIME_DIR/nginx/conf.d/default.conf" ]]; then
|
||||
install -m 0644 \
|
||||
"$WM_DEPLOY_DIR/nginx/bootstrap.conf" \
|
||||
"$WATERMAPS_RUNTIME_DIR/nginx/conf.d/default.conf"
|
||||
fi
|
||||
|
||||
install -m 0644 "$WM_DEPLOY_DIR/systemd/watermaps-route-update.service" /etc/systemd/system/
|
||||
install -m 0644 "$WM_DEPLOY_DIR/systemd/watermaps-route-update.timer" /etc/systemd/system/
|
||||
install -m 0644 "$WM_DEPLOY_DIR/systemd/watermaps-certbot-renew.service" /etc/systemd/system/
|
||||
install -m 0644 "$WM_DEPLOY_DIR/systemd/watermaps-certbot-renew.timer" /etc/systemd/system/
|
||||
systemctl daemon-reload
|
||||
systemctl enable --now watermaps-route-update.timer watermaps-certbot-renew.timer
|
||||
|
||||
wm_log "Server-Bootstrap abgeschlossen."
|
||||
Executable
+363
@@ -0,0 +1,363 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -Eeuo pipefail
|
||||
|
||||
WM_DEPLOY_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
WM_ROOT_DIR="$(cd "$WM_DEPLOY_DIR/.." && pwd)"
|
||||
WM_ENV_FILE="${WATERMAPS_ENV_FILE:-$WM_DEPLOY_DIR/.env.production}"
|
||||
WM_COMPOSE_FILE="$WM_DEPLOY_DIR/compose.production.yml"
|
||||
|
||||
wm_die() {
|
||||
printf 'Fehler: %s\n' "$*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
wm_log() {
|
||||
printf '[watermaps] %s\n' "$*"
|
||||
}
|
||||
|
||||
wm_load_env() {
|
||||
if [[ ! -f "$WM_ENV_FILE" ]]; then
|
||||
wm_die "Konfiguration fehlt: $WM_ENV_FILE (Vorlage: deploy/.env.production.example)"
|
||||
fi
|
||||
|
||||
set -a
|
||||
# shellcheck disable=SC1090
|
||||
source "$WM_ENV_FILE"
|
||||
set +a
|
||||
|
||||
WATERMAPS_DATA_DIR="${WATERMAPS_DATA_DIR:-/srv/watermaps-data}"
|
||||
WATERMAPS_RUNTIME_DIR="${WATERMAPS_RUNTIME_DIR:-/srv/watermaps-runtime}"
|
||||
export WATERMAPS_DATA_DIR WATERMAPS_RUNTIME_DIR
|
||||
|
||||
wm_require_safe_absolute_dir "$WATERMAPS_DATA_DIR"
|
||||
wm_require_safe_absolute_dir "$WATERMAPS_RUNTIME_DIR"
|
||||
}
|
||||
|
||||
wm_require_safe_absolute_dir() {
|
||||
local directory="$1"
|
||||
[[ "$directory" == /* ]] || wm_die "Pfad muss absolut sein: $directory"
|
||||
[[ "$directory" != "/" ]] || wm_die "Das Wurzelverzeichnis darf nicht als Datenpfad verwendet werden."
|
||||
[[ "$directory" != "/srv" ]] || wm_die "Bitte ein Unterverzeichnis von /srv verwenden."
|
||||
}
|
||||
|
||||
wm_assert_data_mount() {
|
||||
mountpoint --quiet "$WATERMAPS_DATA_DIR" ||
|
||||
wm_die "Persistentes Datenvolume ist nicht unter $WATERMAPS_DATA_DIR eingehängt."
|
||||
}
|
||||
|
||||
wm_compose() {
|
||||
docker compose \
|
||||
--project-directory "$WM_ROOT_DIR" \
|
||||
--env-file "$WM_ENV_FILE" \
|
||||
--file "$WM_COMPOSE_FILE" \
|
||||
"$@"
|
||||
}
|
||||
|
||||
wm_route_file() {
|
||||
printf '%s/local/germany-netherlands-fairways.json\n' "$WATERMAPS_DATA_DIR"
|
||||
}
|
||||
|
||||
wm_route_marker() {
|
||||
printf '%s/local/.germany-netherlands-fairways.ready\n' "$WATERMAPS_DATA_DIR"
|
||||
}
|
||||
|
||||
wm_acquire_route_lock() {
|
||||
install -d -m 0755 "$WATERMAPS_RUNTIME_DIR/locks"
|
||||
exec 9>"$WATERMAPS_RUNTIME_DIR/locks/route-update.lock"
|
||||
if ! flock --nonblock 9; then
|
||||
wm_log "Ein Routingdaten-Update oder Deployment läuft bereits."
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
wm_assert_route_data() {
|
||||
wm_route_data_ready || wm_die "Routingdaten sind nicht vollständig bereit."
|
||||
}
|
||||
|
||||
wm_route_data_ready() {
|
||||
wm_route_data_files_ready "$(wm_route_file)" "$(wm_route_marker)"
|
||||
}
|
||||
|
||||
wm_marker_value() {
|
||||
local marker="$1"
|
||||
local requested_key="$2"
|
||||
awk -F= -v requested_key="$requested_key" '
|
||||
$1 == requested_key {
|
||||
value = substr($0, length($1) + 2)
|
||||
matches += 1
|
||||
}
|
||||
END {
|
||||
if (matches != 1) {
|
||||
exit 1
|
||||
}
|
||||
print value
|
||||
}
|
||||
' "$marker"
|
||||
}
|
||||
|
||||
wm_write_route_marker() {
|
||||
local route_file="$1"
|
||||
local marker="$2"
|
||||
local temporary_marker
|
||||
|
||||
[[ -s "$route_file" ]] || return 1
|
||||
chmod 0644 "$route_file"
|
||||
temporary_marker="$(mktemp "$(dirname "$marker")/.routing-ready.XXXXXX")"
|
||||
{
|
||||
printf 'format_version=1\n'
|
||||
printf 'generated_at=%s\n' "$(date --utc +%Y-%m-%dT%H:%M:%SZ)"
|
||||
printf 'route_file_name=%s\n' "$(basename "$route_file")"
|
||||
printf 'size_bytes=%s\n' "$(stat --format=%s "$route_file")"
|
||||
printf 'sha256=%s\n' "$(sha256sum "$route_file" | awk '{ print $1 }')"
|
||||
printf 'generator_version=2\n'
|
||||
printf 'source=germany+netherlands\n'
|
||||
} >"$temporary_marker"
|
||||
chmod 0644 "$temporary_marker"
|
||||
mv -f "$temporary_marker" "$marker"
|
||||
}
|
||||
|
||||
wm_route_data_files_ready() {
|
||||
local route_file="$1"
|
||||
local marker="$2"
|
||||
local file_mode marker_format marker_file_name marker_size marker_checksum
|
||||
local marker_generator marker_source actual_size actual_checksum
|
||||
|
||||
if [[ ! -s "$route_file" ]]; then
|
||||
wm_log "Routingindex fehlt oder ist leer: $route_file"
|
||||
return 1
|
||||
fi
|
||||
if [[ ! -s "$marker" ]]; then
|
||||
wm_log "Bereitschaftsmarker fehlt: $marker"
|
||||
return 1
|
||||
fi
|
||||
if [[ ! "$marker" -nt "$route_file" ]]; then
|
||||
wm_log "Bereitschaftsmarker ist älter als der Routingindex."
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [[ ! -r "$route_file" ]]; then
|
||||
wm_log "Routingindex ist für den prüfenden Benutzer nicht lesbar: $route_file"
|
||||
return 1
|
||||
fi
|
||||
file_mode="$(stat --format=%a "$route_file")"
|
||||
if (( (8#$file_mode & 4) == 0 )); then
|
||||
wm_log "Routingindex ist für den unprivilegierten App-Container nicht lesbar (Modus $file_mode)."
|
||||
return 1
|
||||
fi
|
||||
|
||||
if ! jq --exit-status '
|
||||
(.version == 1)
|
||||
and (.generatorVersion == 2)
|
||||
and (.source == "germany+netherlands")
|
||||
and (.sources | type == "array" and length == 2)
|
||||
and (([.sources[].region] | sort) == ["germany", "netherlands"])
|
||||
and (([.sources[].file] | sort) == [
|
||||
"germany-latest.osm.pbf",
|
||||
"netherlands-latest.osm.pbf"
|
||||
])
|
||||
and (all(.sources[];
|
||||
(.checksumMd5 | type == "string" and test("^[0-9a-f]{32}$"))
|
||||
and (.sizeBytes | type == "number" and . > 0)
|
||||
))
|
||||
and (.ways | type == "array" and length > 0)
|
||||
' "$route_file" >/dev/null; then
|
||||
wm_log "Routingindex enthält nicht exakt die erwarteten Deutschland-/Niederlande-Quellen."
|
||||
return 1
|
||||
fi
|
||||
|
||||
marker_format="$(wm_marker_value "$marker" format_version 2>/dev/null || true)"
|
||||
marker_file_name="$(wm_marker_value "$marker" route_file_name 2>/dev/null || true)"
|
||||
marker_size="$(wm_marker_value "$marker" size_bytes 2>/dev/null || true)"
|
||||
marker_checksum="$(wm_marker_value "$marker" sha256 2>/dev/null || true)"
|
||||
marker_generator="$(wm_marker_value "$marker" generator_version 2>/dev/null || true)"
|
||||
marker_source="$(wm_marker_value "$marker" source 2>/dev/null || true)"
|
||||
actual_size="$(stat --format=%s "$route_file")"
|
||||
actual_checksum="$(sha256sum "$route_file" | awk '{ print $1 }')"
|
||||
|
||||
if [[ "$marker_format" != "1" ||
|
||||
"$marker_file_name" != "$(basename "$route_file")" ||
|
||||
"$marker_size" != "$actual_size" ||
|
||||
! "$marker_checksum" =~ ^[0-9a-f]{64}$ ||
|
||||
"$marker_checksum" != "$actual_checksum" ||
|
||||
"$marker_generator" != "2" ||
|
||||
"$marker_source" != "germany+netherlands" ]]; then
|
||||
wm_log "Bereitschaftsmarker stimmt nicht mit dem Routingindex überein: $marker"
|
||||
return 1
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
wm_wait_for_health() {
|
||||
local service="$1"
|
||||
local timeout_seconds="${2:-180}"
|
||||
local container_id status
|
||||
local deadline=$((SECONDS + timeout_seconds))
|
||||
|
||||
container_id="$(wm_compose ps --quiet "$service")"
|
||||
[[ -n "$container_id" ]] || wm_die "Container für $service läuft nicht."
|
||||
|
||||
while ((SECONDS < deadline)); do
|
||||
status="$(docker inspect --format '{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}' "$container_id")"
|
||||
case "$status" in
|
||||
healthy|running)
|
||||
wm_log "$service ist bereit."
|
||||
return 0
|
||||
;;
|
||||
unhealthy|exited|dead)
|
||||
docker inspect --format '{{json .State}}' "$container_id" >&2 || true
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
sleep 3
|
||||
done
|
||||
|
||||
wm_log "Timeout beim Warten auf $service."
|
||||
return 1
|
||||
}
|
||||
|
||||
wm_smoke_test_route() {
|
||||
wm_compose exec --no-TTY watermaps node --input-type=module --eval '
|
||||
const expectedSource = "local-geofabrik-germany+netherlands";
|
||||
const routeChecks = [
|
||||
{
|
||||
name: "Emden–Ditzum",
|
||||
start: { lat: 53.3422, lon: 7.1871 },
|
||||
destination: { lat: 53.465, lon: 7.4734 },
|
||||
minimumAlternatives: 2
|
||||
},
|
||||
{
|
||||
name: "Norddeich–Norderney",
|
||||
start: { lat: 53.6234, lon: 7.1559 },
|
||||
destination: { lat: 53.7023, lon: 7.1658 },
|
||||
minimumAlternatives: 2
|
||||
},
|
||||
{
|
||||
name: "Emden–Delfzijl",
|
||||
start: { lat: 53.3416, lon: 7.186 },
|
||||
destination: { lat: 53.3282, lon: 6.9304 },
|
||||
minimumAlternatives: 0,
|
||||
minimumCoordinates: 30,
|
||||
minimumDistanceNm: 9.5,
|
||||
maximumDistanceNm: 10.5,
|
||||
maximumSegmentNm: 2,
|
||||
maximumLongitude: 7.19,
|
||||
corridorCoordinates: [
|
||||
[7.1848883, 53.3395697],
|
||||
[7.0011017, 53.313849],
|
||||
[6.9427276, 53.3256427]
|
||||
]
|
||||
},
|
||||
{
|
||||
name: "Weesp–Utrecht",
|
||||
start: { lat: 52.309, lon: 5.0423 },
|
||||
destination: { lat: 52.105, lon: 5.085 },
|
||||
minimumAlternatives: 2
|
||||
},
|
||||
{
|
||||
name: "Lemmer–Sneek",
|
||||
start: { lat: 52.844, lon: 5.71 },
|
||||
destination: { lat: 53.033, lon: 5.66 },
|
||||
minimumAlternatives: 2
|
||||
},
|
||||
{
|
||||
name: "Smal Weesp (Niederlande)",
|
||||
start: { lat: 52.3043984, lon: 5.0210794 },
|
||||
destination: { lat: 52.307897, lon: 5.0330976 },
|
||||
minimumAlternatives: 0
|
||||
}
|
||||
];
|
||||
|
||||
for (const routeCheck of routeChecks) {
|
||||
const response = await fetch("http://127.0.0.1:5174/api/routes", {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
start: routeCheck.start,
|
||||
destination: routeCheck.destination,
|
||||
vesselProfile: { draughtM: 1, safetyReserveM: 0.3 }
|
||||
})
|
||||
});
|
||||
if (!response.ok) {
|
||||
console.error(routeCheck.name, response.status, await response.text());
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const route = await response.json();
|
||||
const alternatives = Array.isArray(route.alternatives) ? route.alternatives : [];
|
||||
const routeCoordinates = route.geometry?.coordinates;
|
||||
const distanceNm = (first, second) => {
|
||||
const toRadians = (value) => value * Math.PI / 180;
|
||||
const deltaLat = toRadians(second[1] - first[1]);
|
||||
const deltaLon = toRadians(second[0] - first[0]);
|
||||
const firstLat = toRadians(first[1]);
|
||||
const secondLat = toRadians(second[1]);
|
||||
const haversine =
|
||||
Math.sin(deltaLat / 2) ** 2
|
||||
+ Math.cos(firstLat) * Math.cos(secondLat) * Math.sin(deltaLon / 2) ** 2;
|
||||
return 3440.065 * 2 * Math.atan2(Math.sqrt(haversine), Math.sqrt(1 - haversine));
|
||||
};
|
||||
const largestSegmentNm = Array.isArray(routeCoordinates)
|
||||
? routeCoordinates.slice(1).reduce(
|
||||
(largest, coordinate, index) =>
|
||||
Math.max(largest, distanceNm(routeCoordinates[index], coordinate)),
|
||||
0
|
||||
)
|
||||
: Number.POSITIVE_INFINITY;
|
||||
const expectedCorridorCoordinates = routeCheck.corridorCoordinates ?? [];
|
||||
const corridorCoordinatesAreValid =
|
||||
expectedCorridorCoordinates.length === 0 ||
|
||||
(
|
||||
Array.isArray(routeCoordinates) &&
|
||||
expectedCorridorCoordinates.every((expectedCoordinate) =>
|
||||
routeCoordinates.some(
|
||||
(coordinate) => distanceNm(coordinate, expectedCoordinate) <= 0.15
|
||||
)
|
||||
)
|
||||
);
|
||||
const alternativeRoutesAreValid = alternatives.every((alternative) =>
|
||||
alternative.routingMode === "fairway"
|
||||
&& Array.isArray(alternative.geometry?.coordinates)
|
||||
&& alternative.geometry.coordinates.length >= 2
|
||||
&& Array.isArray(alternative.dataSources)
|
||||
&& alternative.dataSources.includes(expectedSource)
|
||||
);
|
||||
const routeSignatures = [
|
||||
route.geometry?.coordinates,
|
||||
...alternatives.map((alternative) => alternative.geometry?.coordinates)
|
||||
].map((coordinates) => JSON.stringify(coordinates));
|
||||
if (
|
||||
route.routingMode !== "fairway" ||
|
||||
!Array.isArray(routeCoordinates) ||
|
||||
routeCoordinates.length < (routeCheck.minimumCoordinates ?? 2) ||
|
||||
!Array.isArray(route.dataSources) ||
|
||||
!route.dataSources.includes(expectedSource) ||
|
||||
alternatives.length < routeCheck.minimumAlternatives ||
|
||||
route.distanceNm < (routeCheck.minimumDistanceNm ?? 0) ||
|
||||
route.distanceNm > (routeCheck.maximumDistanceNm ?? Number.POSITIVE_INFINITY) ||
|
||||
largestSegmentNm > (routeCheck.maximumSegmentNm ?? Number.POSITIVE_INFINITY) ||
|
||||
routeCoordinates.some(
|
||||
([lon]) => lon > (routeCheck.maximumLongitude ?? Number.POSITIVE_INFINITY)
|
||||
) ||
|
||||
!corridorCoordinatesAreValid ||
|
||||
!alternativeRoutesAreValid ||
|
||||
new Set(routeSignatures).size !== routeSignatures.length
|
||||
) {
|
||||
console.error(
|
||||
`${routeCheck.name}: Routen- oder Alternativenprüfung des lokalen Deutschland-/Niederlande-Index fehlgeschlagen.`,
|
||||
JSON.stringify({
|
||||
routingMode: route.routingMode,
|
||||
dataSources: route.dataSources,
|
||||
distanceNm: route.distanceNm,
|
||||
coordinateCount: routeCoordinates?.length,
|
||||
largestSegmentNm,
|
||||
alternativeCount: alternatives.length,
|
||||
minimumAlternatives: routeCheck.minimumAlternatives,
|
||||
corridorCoordinatesAreValid
|
||||
})
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
'
|
||||
}
|
||||
Executable
+58
@@ -0,0 +1,58 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -Eeuo pipefail
|
||||
|
||||
WM_DEPLOY_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
# shellcheck source=common.sh
|
||||
source "$WM_DEPLOY_DIR/scripts/common.sh"
|
||||
wm_load_env
|
||||
wm_assert_data_mount
|
||||
|
||||
if [[ "$(id -u)" -ne 0 ]]; then
|
||||
wm_die "Dieses Skript muss als root ausgeführt werden."
|
||||
fi
|
||||
|
||||
install -d -m 0755 \
|
||||
"$WATERMAPS_DATA_DIR/geofabrik" \
|
||||
"$WATERMAPS_DATA_DIR/local" \
|
||||
"$WATERMAPS_DATA_DIR/certbot/www" \
|
||||
"$WATERMAPS_DATA_DIR/certbot/letsencrypt" \
|
||||
"$WATERMAPS_RUNTIME_DIR/nginx/conf.d" \
|
||||
"$WATERMAPS_RUNTIME_DIR/locks"
|
||||
|
||||
wm_acquire_route_lock ||
|
||||
wm_die "Deployment abgebrochen, weil gerade Routingdaten aktualisiert werden."
|
||||
|
||||
if [[ ! -f "$WATERMAPS_RUNTIME_DIR/nginx/conf.d/default.conf" ]]; then
|
||||
install -m 0644 \
|
||||
"$WM_DEPLOY_DIR/nginx/bootstrap.conf" \
|
||||
"$WATERMAPS_RUNTIME_DIR/nginx/conf.d/default.conf"
|
||||
fi
|
||||
|
||||
wm_log "Anwendungsimage wird gebaut."
|
||||
wm_compose build watermaps
|
||||
wm_compose up --detach --remove-orphans watermaps nginx
|
||||
|
||||
wm_wait_for_health watermaps 240
|
||||
wm_wait_for_health nginx 120
|
||||
|
||||
route_rebuild_required=false
|
||||
update_args=()
|
||||
if [[ "${WATERMAPS_REBUILD_ROUTE_DATA:-false}" == "true" ]]; then
|
||||
route_rebuild_required=true
|
||||
update_args+=(--force)
|
||||
elif ! wm_route_data_ready; then
|
||||
route_rebuild_required=true
|
||||
fi
|
||||
|
||||
if [[ "$route_rebuild_required" == "true" ]]; then
|
||||
wm_log "Deutschland- und Niederlande-Routingdaten werden sicher vorbereitet."
|
||||
WATERMAPS_ROUTE_LOCK_HELD=true \
|
||||
"$WM_DEPLOY_DIR/scripts/update-route-data.sh" "${update_args[@]}"
|
||||
fi
|
||||
|
||||
wm_assert_route_data
|
||||
wm_wait_for_health watermaps 240
|
||||
wm_smoke_test_route
|
||||
|
||||
wm_log "Deployment ist bereit. Vor dem Livegang liefert Port 80 nur ACME-Challenges und ansonsten HTTP 404."
|
||||
Executable
+115
@@ -0,0 +1,115 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -Eeuo pipefail
|
||||
|
||||
WM_DEPLOY_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
# shellcheck source=common.sh
|
||||
source "$WM_DEPLOY_DIR/scripts/common.sh"
|
||||
wm_load_env
|
||||
wm_assert_data_mount
|
||||
|
||||
if [[ "$(id -u)" -ne 0 ]]; then
|
||||
wm_die "Dieses Skript muss als root ausgeführt werden."
|
||||
fi
|
||||
|
||||
domain="${WATERMAPS_DOMAIN:-}"
|
||||
email="${WATERMAPS_ACME_EMAIL:-}"
|
||||
expected_ipv4="${1:-${WATERMAPS_EXPECTED_IPV4:-}}"
|
||||
|
||||
[[ "$domain" =~ ^([A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?\.)+[A-Za-z]{2,63}$ ]] ||
|
||||
wm_die "Ungültiger WATERMAPS_DOMAIN: $domain"
|
||||
[[ "$email" == *@*.* && "$email" != "admin@example.com" ]] ||
|
||||
wm_die "Bitte WATERMAPS_ACME_EMAIL in deploy/.env.production konfigurieren."
|
||||
[[ "$expected_ipv4" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]] ||
|
||||
wm_die "Erwartete Server-IPv4 fehlt. Als Argument übergeben oder WATERMAPS_EXPECTED_IPV4 setzen."
|
||||
|
||||
wm_assert_route_data
|
||||
wm_wait_for_health watermaps 120
|
||||
wm_wait_for_health nginx 60
|
||||
wm_smoke_test_route
|
||||
|
||||
mapfile -t resolved_ipv4s < <(dig +short A "$domain" | awk '/^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$/')
|
||||
[[ "${#resolved_ipv4s[@]}" -gt 0 ]] || wm_die "Für $domain ist noch kein A-Record auflösbar."
|
||||
|
||||
for resolved_ipv4 in "${resolved_ipv4s[@]}"; do
|
||||
[[ "$resolved_ipv4" == "$expected_ipv4" ]] ||
|
||||
wm_die "$domain zeigt zusätzlich/abweichend auf $resolved_ipv4 statt ausschließlich auf $expected_ipv4."
|
||||
done
|
||||
|
||||
challenge_name="watermaps-preflight-$$"
|
||||
challenge_dir="$WATERMAPS_DATA_DIR/certbot/www/.well-known/acme-challenge"
|
||||
challenge_file="$challenge_dir/$challenge_name"
|
||||
install -d -m 0755 "$challenge_dir"
|
||||
printf 'watermaps-acme-preflight\n' >"$challenge_file"
|
||||
trap 'rm -f "$challenge_file"' EXIT
|
||||
|
||||
curl --fail --silent --show-error \
|
||||
--header "Host: $domain" \
|
||||
"http://127.0.0.1/.well-known/acme-challenge/$challenge_name" \
|
||||
| grep -qx 'watermaps-acme-preflight' ||
|
||||
wm_die "Nginx stellt den ACME-Webroot nicht korrekt bereit."
|
||||
|
||||
certbot_args=(
|
||||
certonly
|
||||
--webroot
|
||||
--webroot-path /var/www/certbot
|
||||
--domain "$domain"
|
||||
--email "$email"
|
||||
--agree-tos
|
||||
--non-interactive
|
||||
--keep-until-expiring
|
||||
)
|
||||
cert_name="$domain"
|
||||
if [[ "${WATERMAPS_CERTBOT_STAGING:-false}" == "true" ]]; then
|
||||
cert_name="${domain}-staging"
|
||||
certbot_args+=(--staging)
|
||||
fi
|
||||
certbot_args+=(--cert-name "$cert_name")
|
||||
|
||||
wm_log "Let's-Encrypt-Zertifikat wird angefordert."
|
||||
wm_compose --profile maintenance run --rm certbot "${certbot_args[@]}"
|
||||
|
||||
certificate_path="$WATERMAPS_DATA_DIR/certbot/letsencrypt/live/$cert_name/fullchain.pem"
|
||||
private_key_path="$WATERMAPS_DATA_DIR/certbot/letsencrypt/live/$cert_name/privkey.pem"
|
||||
[[ -s "$certificate_path" && -s "$private_key_path" ]] ||
|
||||
wm_die "Certbot war beendet, aber Zertifikat oder privater Schlüssel fehlen."
|
||||
|
||||
candidate="$(mktemp "$WATERMAPS_RUNTIME_DIR/nginx/conf.d/.https.XXXXXX")"
|
||||
trap 'rm -f "$challenge_file" "$candidate"' EXIT
|
||||
sed \
|
||||
-e "s/__WATERMAPS_DOMAIN__/$domain/g" \
|
||||
-e "s/__WATERMAPS_CERT_NAME__/$cert_name/g" \
|
||||
"$WM_DEPLOY_DIR/nginx/https.conf.template" \
|
||||
>"$candidate"
|
||||
|
||||
wm_compose run --rm --no-deps \
|
||||
--volume "$candidate:/etc/nginx/conf.d/default.conf:ro" \
|
||||
nginx nginx -t
|
||||
|
||||
active_config="$WATERMAPS_RUNTIME_DIR/nginx/conf.d/default.conf"
|
||||
previous_config="$WATERMAPS_RUNTIME_DIR/nginx/conf.d/.default.conf.previous"
|
||||
cp --preserve=mode,timestamps "$active_config" "$previous_config"
|
||||
install -m 0644 "$candidate" "$active_config"
|
||||
|
||||
if ! wm_compose exec --no-TTY nginx nginx -t; then
|
||||
install -m 0644 "$previous_config" "$active_config"
|
||||
wm_compose exec --no-TTY nginx nginx -s reload || true
|
||||
wm_die "HTTPS-Konfiguration war ungültig; Bootstrap-Konfiguration wurde wiederhergestellt."
|
||||
fi
|
||||
wm_compose exec --no-TTY nginx nginx -s reload
|
||||
|
||||
https_curl_args=(--fail --silent --show-error)
|
||||
if [[ "${WATERMAPS_CERTBOT_STAGING:-false}" == "true" ]]; then
|
||||
https_curl_args+=(--insecure)
|
||||
fi
|
||||
if ! curl "${https_curl_args[@]}" \
|
||||
--resolve "$domain:443:$expected_ipv4" \
|
||||
"https://$domain/health" \
|
||||
>/dev/null; then
|
||||
install -m 0644 "$previous_config" "$active_config"
|
||||
wm_compose exec --no-TTY nginx nginx -t
|
||||
wm_compose exec --no-TTY nginx nginx -s reload
|
||||
wm_die "HTTPS-Smoke-Test fehlgeschlagen; vorherige Nginx-Konfiguration wurde wiederhergestellt."
|
||||
fi
|
||||
|
||||
wm_log "Livegang erfolgreich: https://$domain"
|
||||
Executable
+38
@@ -0,0 +1,38 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -Eeuo pipefail
|
||||
|
||||
cd /workspace
|
||||
|
||||
route_file="${WATERMAPS_LOCAL_FAIRWAYS_PATH:-/workspace/data/local/germany-netherlands-fairways.json}"
|
||||
marker_file="${WATERMAPS_ROUTE_MARKER_PATH:-$(dirname "$route_file")/.germany-netherlands-fairways.ready}"
|
||||
|
||||
mkdir -p "$(dirname "$route_file")" /workspace/data/geofabrik
|
||||
|
||||
if [[ ! -x /workspace/scripts/setup-local-routing.sh ]]; then
|
||||
printf 'Fehler: scripts/setup-local-routing.sh fehlt oder ist nicht ausführbar.\n' >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
/workspace/scripts/setup-local-routing.sh
|
||||
|
||||
if [[ ! -s "$route_file" ]]; then
|
||||
printf 'Fehler: Der Routingindex wurde nicht erzeugt: %s\n' "$route_file" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
chmod 0644 "$route_file"
|
||||
temporary_marker="$(mktemp "$(dirname "$marker_file")/.routing-ready.XXXXXX")"
|
||||
{
|
||||
printf 'format_version=1\n'
|
||||
printf 'generated_at=%s\n' "$(date --utc +%Y-%m-%dT%H:%M:%SZ)"
|
||||
printf 'route_file_name=%s\n' "$(basename "$route_file")"
|
||||
printf 'size_bytes=%s\n' "$(stat --format=%s "$route_file")"
|
||||
printf 'sha256=%s\n' "$(sha256sum "$route_file" | awk '{ print $1 }')"
|
||||
printf 'generator_version=2\n'
|
||||
printf 'source=germany+netherlands\n'
|
||||
} >"$temporary_marker"
|
||||
chmod 0644 "$temporary_marker"
|
||||
mv -f "$temporary_marker" "$marker_file"
|
||||
|
||||
printf 'Routingdaten sind bereit: %s\n' "$route_file"
|
||||
Executable
+77
@@ -0,0 +1,77 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -Eeuo pipefail
|
||||
|
||||
WM_LOCAL_DEPLOY_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
WM_LOCAL_ROOT_DIR="$(cd "$WM_LOCAL_DEPLOY_DIR/.." && pwd)"
|
||||
WM_LOCAL_INFRA_DIR="${WATERMAPS_INFRA_DIR:-$WM_LOCAL_ROOT_DIR/infra/opentofu}"
|
||||
|
||||
wm_local_die() {
|
||||
printf 'Fehler: %s\n' "$*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
wm_local_resolve_ssh() {
|
||||
local requested_server="${1:-}"
|
||||
local requested_identity="${2:-}"
|
||||
|
||||
WM_SERVER_IPV4="${requested_server:-${WATERMAPS_SERVER_IPV4:-}}"
|
||||
if [[ -z "$WM_SERVER_IPV4" ]]; then
|
||||
command -v tofu >/dev/null 2>&1 ||
|
||||
wm_local_die "OpenTofu fehlt und WATERMAPS_SERVER_IPV4 wurde nicht gesetzt."
|
||||
[[ -d "$WM_LOCAL_INFRA_DIR" ]] ||
|
||||
wm_local_die "OpenTofu-Verzeichnis fehlt: $WM_LOCAL_INFRA_DIR"
|
||||
WM_SERVER_IPV4="$(tofu -chdir="$WM_LOCAL_INFRA_DIR" output -raw server_ipv4)"
|
||||
fi
|
||||
[[ "$WM_SERVER_IPV4" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]] ||
|
||||
wm_local_die "Ungültige Server-IPv4: $WM_SERVER_IPV4"
|
||||
|
||||
WM_SSH_IDENTITY="${requested_identity:-${WATERMAPS_SSH_KEY:-}}"
|
||||
if [[ -z "$WM_SSH_IDENTITY" ]] && command -v tofu >/dev/null 2>&1 && [[ -d "$WM_LOCAL_INFRA_DIR" ]]; then
|
||||
candidate_identity="$(tofu -chdir="$WM_LOCAL_INFRA_DIR" output -raw ssh_private_key_path 2>/dev/null || true)"
|
||||
if [[ -n "$candidate_identity" && -f "$candidate_identity" ]]; then
|
||||
WM_SSH_IDENTITY="$candidate_identity"
|
||||
fi
|
||||
fi
|
||||
if [[ -n "$WM_SSH_IDENTITY" && ! -f "$WM_SSH_IDENTITY" ]]; then
|
||||
wm_local_die "SSH-Keydatei nicht gefunden: $WM_SSH_IDENTITY"
|
||||
fi
|
||||
|
||||
WM_SSH_USER="${WATERMAPS_SSH_USER:-deploy}"
|
||||
[[ "$WM_SSH_USER" =~ ^[a-z_][a-z0-9_-]{0,30}$ ]] ||
|
||||
wm_local_die "Ungültiger SSH-Benutzer: $WM_SSH_USER"
|
||||
if [[ -n "${WATERMAPS_REMOTE_SUDO+x}" ]]; then
|
||||
WM_REMOTE_SUDO="$WATERMAPS_REMOTE_SUDO"
|
||||
elif [[ "$WM_SSH_USER" == "root" ]]; then
|
||||
WM_REMOTE_SUDO=""
|
||||
else
|
||||
WM_REMOTE_SUDO="sudo"
|
||||
fi
|
||||
case "$WM_REMOTE_SUDO" in
|
||||
""|sudo) ;;
|
||||
*) wm_local_die "WATERMAPS_REMOTE_SUDO darf nur leer oder 'sudo' sein." ;;
|
||||
esac
|
||||
|
||||
WM_SSH_TARGET="$WM_SSH_USER@$WM_SERVER_IPV4"
|
||||
WM_SSH_OPTIONS=(-o BatchMode=yes -o StrictHostKeyChecking=accept-new)
|
||||
if [[ -n "$WM_SSH_IDENTITY" ]]; then
|
||||
WM_SSH_OPTIONS+=(-i "$WM_SSH_IDENTITY" -o IdentitiesOnly=yes)
|
||||
fi
|
||||
}
|
||||
|
||||
wm_local_wait_for_ssh() {
|
||||
local timeout_seconds="${WATERMAPS_SSH_WAIT_SECONDS:-600}"
|
||||
local deadline=$((SECONDS + timeout_seconds))
|
||||
|
||||
while ((SECONDS < deadline)); do
|
||||
if ssh "${WM_SSH_OPTIONS[@]}" \
|
||||
-o ConnectTimeout=5 \
|
||||
"$WM_SSH_TARGET" true >/dev/null 2>&1; then
|
||||
return 0
|
||||
fi
|
||||
printf '[watermaps] Warte auf SSH und Cloud-init-Benutzer %s …\n' "$WM_SSH_TARGET"
|
||||
sleep 5
|
||||
done
|
||||
|
||||
wm_local_die "SSH war nach ${timeout_seconds}s nicht erreichbar: $WM_SSH_TARGET"
|
||||
}
|
||||
Executable
+48
@@ -0,0 +1,48 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -Eeuo pipefail
|
||||
|
||||
WM_DEPLOY_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
# shellcheck source=remote-common.sh
|
||||
source "$WM_DEPLOY_DIR/scripts/remote-common.sh"
|
||||
|
||||
server_ipv4=""
|
||||
identity_file=""
|
||||
|
||||
while [[ "$#" -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--host)
|
||||
[[ "$#" -ge 2 ]] || wm_local_die "Wert für --host fehlt."
|
||||
server_ipv4="$2"
|
||||
shift 2
|
||||
;;
|
||||
--identity)
|
||||
[[ "$#" -ge 2 ]] || wm_local_die "Wert für --identity fehlt."
|
||||
identity_file="$2"
|
||||
shift 2
|
||||
;;
|
||||
--user)
|
||||
[[ "$#" -ge 2 ]] || wm_local_die "Wert für --user fehlt."
|
||||
WATERMAPS_SSH_USER="$2"
|
||||
export WATERMAPS_SSH_USER
|
||||
shift 2
|
||||
;;
|
||||
-h|--help)
|
||||
printf 'Verwendung: %s [--host IPV4] [--identity DATEI] [--user BENUTZER]\n' "$0"
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
wm_local_die "Unbekannte Option: $1"
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
wm_local_resolve_ssh "$server_ipv4" "$identity_file"
|
||||
wm_local_wait_for_ssh
|
||||
remote_prefix=""
|
||||
if [[ -n "$WM_REMOTE_SUDO" ]]; then
|
||||
remote_prefix="$WM_REMOTE_SUDO "
|
||||
fi
|
||||
|
||||
ssh "${WM_SSH_OPTIONS[@]}" "$WM_SSH_TARGET" \
|
||||
"${remote_prefix}/opt/watermaps/deploy/scripts/go-live.sh '$WM_SERVER_IPV4'"
|
||||
Executable
+33
@@ -0,0 +1,33 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -Eeuo pipefail
|
||||
|
||||
WM_DEPLOY_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
# shellcheck source=common.sh
|
||||
source "$WM_DEPLOY_DIR/scripts/common.sh"
|
||||
wm_load_env
|
||||
wm_assert_data_mount
|
||||
|
||||
if [[ "$(id -u)" -ne 0 ]]; then
|
||||
wm_die "Dieses Skript muss als root ausgeführt werden."
|
||||
fi
|
||||
|
||||
domain="${WATERMAPS_DOMAIN:-}"
|
||||
cert_name="$domain"
|
||||
if [[ "${WATERMAPS_CERTBOT_STAGING:-false}" == "true" ]]; then
|
||||
cert_name="${domain}-staging"
|
||||
fi
|
||||
[[ -s "$WATERMAPS_DATA_DIR/certbot/letsencrypt/live/$cert_name/fullchain.pem" ]] || {
|
||||
wm_log "Noch kein Zertifikat vorhanden; Erneuerung wird übersprungen."
|
||||
exit 0
|
||||
}
|
||||
|
||||
wm_compose --profile maintenance run --rm certbot \
|
||||
renew \
|
||||
--webroot \
|
||||
--webroot-path /var/www/certbot \
|
||||
--quiet
|
||||
|
||||
wm_compose exec --no-TTY nginx nginx -t
|
||||
wm_compose exec --no-TTY nginx nginx -s reload
|
||||
wm_log "Zertifikatserneuerung geprüft und Nginx neu geladen."
|
||||
+74
@@ -0,0 +1,74 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -Eeuo pipefail
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../.." && pwd)"
|
||||
TEST_DIR="$(mktemp -d)"
|
||||
trap 'rm -rf "$TEST_DIR"' EXIT
|
||||
|
||||
export WATERMAPS_DATA_DIR="$TEST_DIR/data"
|
||||
export WATERMAPS_RUNTIME_DIR="$TEST_DIR/runtime"
|
||||
mkdir -p "$WATERMAPS_DATA_DIR/local" "$WATERMAPS_RUNTIME_DIR"
|
||||
|
||||
# shellcheck source=../common.sh
|
||||
source "$ROOT_DIR/deploy/scripts/common.sh"
|
||||
|
||||
route_file="$(wm_route_file)"
|
||||
route_marker="$(wm_route_marker)"
|
||||
|
||||
write_valid_route() {
|
||||
cat >"$route_file" <<'JSON'
|
||||
{
|
||||
"version": 1,
|
||||
"generatorVersion": 2,
|
||||
"source": "germany+netherlands",
|
||||
"sources": [
|
||||
{
|
||||
"region": "germany",
|
||||
"file": "germany-latest.osm.pbf",
|
||||
"checksumMd5": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||
"sizeBytes": 1
|
||||
},
|
||||
{
|
||||
"region": "netherlands",
|
||||
"file": "netherlands-latest.osm.pbf",
|
||||
"checksumMd5": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
|
||||
"sizeBytes": 1
|
||||
}
|
||||
],
|
||||
"ways": [{"id": "1"}]
|
||||
}
|
||||
JSON
|
||||
chmod 0644 "$route_file"
|
||||
}
|
||||
|
||||
write_valid_route
|
||||
wm_write_route_marker "$route_file" "$route_marker"
|
||||
wm_route_data_ready
|
||||
|
||||
sed -i 's/^size_bytes=.*/size_bytes=1/' "$route_marker"
|
||||
if wm_route_data_ready; then
|
||||
printf 'Ungültige Markergröße wurde akzeptiert.\n' >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
wm_write_route_marker "$route_file" "$route_marker"
|
||||
jq '.sources = [.sources[0]] | .source = "germany"' \
|
||||
"$route_file" >"$route_file.tmp"
|
||||
mv "$route_file.tmp" "$route_file"
|
||||
chmod 0644 "$route_file"
|
||||
wm_write_route_marker "$route_file" "$route_marker"
|
||||
if wm_route_data_ready; then
|
||||
printf 'Deutschland-only-Index wurde akzeptiert.\n' >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
write_valid_route
|
||||
wm_write_route_marker "$route_file" "$route_marker"
|
||||
chmod 0600 "$route_file"
|
||||
if wm_route_data_ready; then
|
||||
printf 'Für den App-Container unlesbarer Index wurde akzeptiert.\n' >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
printf 'Routingdaten- und Markerprüfungen: OK\n'
|
||||
+153
@@ -0,0 +1,153 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -Eeuo pipefail
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../.." && pwd)"
|
||||
TEST_DIR="$(mktemp -d)"
|
||||
trap 'rm -rf "$TEST_DIR"' EXIT
|
||||
|
||||
FAKE_BIN="$TEST_DIR/bin"
|
||||
DATA_DIR="$TEST_DIR/data"
|
||||
RUNTIME_DIR="$TEST_DIR/runtime"
|
||||
ENV_FILE="$TEST_DIR/production.env"
|
||||
mkdir -p "$FAKE_BIN" "$DATA_DIR/local" "$RUNTIME_DIR"
|
||||
|
||||
cat >"$ENV_FILE" <<EOF
|
||||
WATERMAPS_DATA_DIR=$DATA_DIR
|
||||
WATERMAPS_RUNTIME_DIR=$RUNTIME_DIR
|
||||
EOF
|
||||
|
||||
cat >"$FAKE_BIN/id" <<'SH'
|
||||
#!/usr/bin/env bash
|
||||
if [[ "${1:-}" == "-u" ]]; then
|
||||
printf '0\n'
|
||||
else
|
||||
exec /usr/bin/id "$@"
|
||||
fi
|
||||
SH
|
||||
|
||||
cat >"$FAKE_BIN/mountpoint" <<'SH'
|
||||
#!/usr/bin/env bash
|
||||
exit 0
|
||||
SH
|
||||
|
||||
cat >"$FAKE_BIN/docker" <<'SH'
|
||||
#!/usr/bin/env bash
|
||||
set -Eeuo pipefail
|
||||
|
||||
if [[ "${1:-}" == "inspect" ]]; then
|
||||
printf 'healthy\n'
|
||||
exit 0
|
||||
fi
|
||||
|
||||
arguments=" $* "
|
||||
if [[ "$arguments" == *" ps --quiet watermaps "* ]]; then
|
||||
printf 'watermaps-test-container\n'
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [[ "$arguments" == *" run "*" route-data "* ||
|
||||
"$arguments" == *" run "*" route-data" ]]; then
|
||||
route_file="$WATERMAPS_DATA_DIR/local/.germany-netherlands-fairways.candidate.json"
|
||||
marker_file="$WATERMAPS_DATA_DIR/local/.germany-netherlands-fairways.candidate.ready"
|
||||
cat >"$route_file" <<JSON
|
||||
{
|
||||
"version": 1,
|
||||
"generatorVersion": 2,
|
||||
"source": "germany+netherlands",
|
||||
"revision": ${FAKE_ROUTE_REVISION:-1},
|
||||
"sources": [
|
||||
{
|
||||
"region": "germany",
|
||||
"file": "germany-latest.osm.pbf",
|
||||
"checksumMd5": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||
"sizeBytes": 1
|
||||
},
|
||||
{
|
||||
"region": "netherlands",
|
||||
"file": "netherlands-latest.osm.pbf",
|
||||
"checksumMd5": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
|
||||
"sizeBytes": 1
|
||||
}
|
||||
],
|
||||
"ways": [{"id": "1"}]
|
||||
}
|
||||
JSON
|
||||
# shellcheck source=../common.sh
|
||||
source "$WM_TEST_ROOT/deploy/scripts/common.sh"
|
||||
wm_write_route_marker "$route_file" "$marker_file"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [[ "$arguments" == *" exec --no-TTY watermaps "* ]]; then
|
||||
active_file="$WATERMAPS_DATA_DIR/local/germany-netherlands-fairways.json"
|
||||
active_revision="$(jq --raw-output '.revision // 0' "$active_file" 2>/dev/null || true)"
|
||||
if [[ -n "${FAKE_FAIL_REVISION:-}" && "$active_revision" == "$FAKE_FAIL_REVISION" ]]; then
|
||||
exit 1
|
||||
fi
|
||||
exit 0
|
||||
fi
|
||||
|
||||
exit 0
|
||||
SH
|
||||
|
||||
chmod +x "$FAKE_BIN/id" "$FAKE_BIN/mountpoint" "$FAKE_BIN/docker"
|
||||
|
||||
export PATH="$FAKE_BIN:$PATH"
|
||||
export WATERMAPS_ENV_FILE="$ENV_FILE"
|
||||
export WM_TEST_ROOT="$ROOT_DIR"
|
||||
|
||||
# A first successful activation creates an active index and a validated marker.
|
||||
FAKE_ROUTE_REVISION=1 "$ROOT_DIR/deploy/scripts/update-route-data.sh"
|
||||
active_file="$DATA_DIR/local/germany-netherlands-fairways.json"
|
||||
active_marker="$DATA_DIR/local/.germany-netherlands-fairways.ready"
|
||||
[[ "$(jq --raw-output '.revision' "$active_file")" == "1" ]]
|
||||
|
||||
export WATERMAPS_DATA_DIR="$DATA_DIR"
|
||||
export WATERMAPS_RUNTIME_DIR="$RUNTIME_DIR"
|
||||
# shellcheck source=../common.sh
|
||||
source "$ROOT_DIR/deploy/scripts/common.sh"
|
||||
wm_route_data_files_ready "$active_file" "$active_marker"
|
||||
|
||||
# A candidate that fails its route smoke test must restore and retest revision 1.
|
||||
set +e
|
||||
FAKE_ROUTE_REVISION=2 FAKE_FAIL_REVISION=2 \
|
||||
"$ROOT_DIR/deploy/scripts/update-route-data.sh"
|
||||
update_status=$?
|
||||
set -e
|
||||
[[ "$update_status" -ne 0 ]]
|
||||
[[ "$(jq --raw-output '.revision' "$active_file")" == "1" ]]
|
||||
wm_route_data_files_ready "$active_file" "$active_marker"
|
||||
[[ ! -e "$RUNTIME_DIR/locks/route-update.transaction" ]]
|
||||
|
||||
# A hard-crash marker restores the durable previous index before any new build.
|
||||
backup_file="$DATA_DIR/local/.germany-netherlands-fairways.previous.json"
|
||||
backup_marker="$DATA_DIR/local/.germany-netherlands-fairways.previous.ready"
|
||||
cp "$active_file" "$backup_file"
|
||||
wm_write_route_marker "$backup_file" "$backup_marker"
|
||||
jq '.revision = 9' "$active_file" >"$active_file.tmp"
|
||||
mv "$active_file.tmp" "$active_file"
|
||||
chmod 0644 "$active_file"
|
||||
wm_write_route_marker "$active_file" "$active_marker"
|
||||
printf 'had_previous=true\n' >"$RUNTIME_DIR/locks/route-update.transaction"
|
||||
|
||||
FAKE_ROUTE_REVISION=1 "$ROOT_DIR/deploy/scripts/update-route-data.sh"
|
||||
[[ "$(jq --raw-output '.revision' "$active_file")" == "1" ]]
|
||||
wm_route_data_files_ready "$active_file" "$active_marker"
|
||||
[[ ! -e "$RUNTIME_DIR/locks/route-update.transaction" ]]
|
||||
|
||||
# Without a previous valid index, a failed first candidate must not stay ready.
|
||||
rm -f "$active_file" "$active_marker" \
|
||||
"$DATA_DIR/local/.germany-netherlands-fairways.previous.json" \
|
||||
"$DATA_DIR/local/.germany-netherlands-fairways.previous.ready"
|
||||
set +e
|
||||
FAKE_ROUTE_REVISION=3 FAKE_FAIL_REVISION=3 \
|
||||
"$ROOT_DIR/deploy/scripts/update-route-data.sh"
|
||||
first_update_status=$?
|
||||
set -e
|
||||
[[ "$first_update_status" -ne 0 ]]
|
||||
[[ ! -e "$active_file" ]]
|
||||
[[ ! -e "$active_marker" ]]
|
||||
[[ ! -e "$RUNTIME_DIR/locks/route-update.transaction" ]]
|
||||
|
||||
printf 'Staging-, Rollback- und First-Update-Prüfungen: OK\n'
|
||||
Executable
+207
@@ -0,0 +1,207 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -Eeuo pipefail
|
||||
|
||||
WM_DEPLOY_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
# shellcheck source=common.sh
|
||||
source "$WM_DEPLOY_DIR/scripts/common.sh"
|
||||
wm_load_env
|
||||
wm_assert_data_mount
|
||||
|
||||
if [[ "$(id -u)" -ne 0 ]]; then
|
||||
wm_die "Dieses Skript muss als root ausgeführt werden."
|
||||
fi
|
||||
|
||||
force_rebuild=false
|
||||
if [[ "${1:-}" == "--force" ]]; then
|
||||
force_rebuild=true
|
||||
shift
|
||||
fi
|
||||
[[ "$#" -eq 0 ]] || wm_die "Unbekannte Argumente für update-route-data.sh."
|
||||
|
||||
install -d -m 0755 "$WATERMAPS_RUNTIME_DIR/locks" "$WATERMAPS_DATA_DIR/local"
|
||||
if [[ "${WATERMAPS_ROUTE_LOCK_HELD:-false}" != "true" ]]; then
|
||||
if ! wm_acquire_route_lock; then
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
|
||||
route_file="$(wm_route_file)"
|
||||
route_marker="$(wm_route_marker)"
|
||||
route_dir="$(dirname "$route_file")"
|
||||
candidate_file="$route_dir/.germany-netherlands-fairways.candidate.json"
|
||||
candidate_marker="$route_dir/.germany-netherlands-fairways.candidate.ready"
|
||||
backup_file="$route_dir/.germany-netherlands-fairways.previous.json"
|
||||
backup_marker="$route_dir/.germany-netherlands-fairways.previous.ready"
|
||||
transaction_file="$WATERMAPS_RUNTIME_DIR/locks/route-update.transaction"
|
||||
container_candidate="/workspace/data/local/$(basename "$candidate_file")"
|
||||
container_candidate_marker="/workspace/data/local/$(basename "$candidate_marker")"
|
||||
|
||||
transaction_active=false
|
||||
rollback_restored_previous=false
|
||||
|
||||
wm_copy_route_atomically() {
|
||||
local source_file="$1"
|
||||
local destination_file="$2"
|
||||
local temporary_file
|
||||
|
||||
temporary_file="$(mktemp "$(dirname "$destination_file")/.route-copy.XXXXXX")"
|
||||
if ! cp --reflink=auto "$source_file" "$temporary_file"; then
|
||||
rm -f "$temporary_file"
|
||||
return 1
|
||||
fi
|
||||
chmod 0644 "$temporary_file"
|
||||
mv -f "$temporary_file" "$destination_file"
|
||||
}
|
||||
|
||||
wm_watermaps_is_running() {
|
||||
[[ -n "$(wm_compose ps --quiet watermaps)" ]]
|
||||
}
|
||||
|
||||
wm_restore_previous_route() {
|
||||
rollback_restored_previous=false
|
||||
if [[ -s "$backup_file" && -s "$backup_marker" ]] &&
|
||||
wm_route_data_files_ready "$backup_file" "$backup_marker"; then
|
||||
if ! wm_copy_route_atomically "$backup_file" "$route_file" ||
|
||||
! wm_write_route_marker "$route_file" "$route_marker" ||
|
||||
! wm_route_data_ready; then
|
||||
rm -f "$route_file" "$route_marker"
|
||||
return 1
|
||||
fi
|
||||
rollback_restored_previous=true
|
||||
return 0
|
||||
fi
|
||||
|
||||
rm -f "$route_file" "$route_marker"
|
||||
return 1
|
||||
}
|
||||
|
||||
wm_restart_after_restore() {
|
||||
if ! wm_watermaps_is_running; then
|
||||
wm_log "Watermaps läuft nicht; wiederhergestellte Routingdaten können noch nicht getestet werden."
|
||||
return 1
|
||||
fi
|
||||
|
||||
wm_compose restart watermaps
|
||||
wm_wait_for_health watermaps 240 || return 1
|
||||
if [[ "$rollback_restored_previous" == "true" ]]; then
|
||||
wm_smoke_test_route
|
||||
fi
|
||||
}
|
||||
|
||||
wm_rollback_transaction() {
|
||||
local restore_status=0
|
||||
|
||||
if ! wm_restore_previous_route; then
|
||||
restore_status=1
|
||||
fi
|
||||
rm -f "$transaction_file"
|
||||
transaction_active=false
|
||||
|
||||
if ! wm_restart_after_restore; then
|
||||
return 1
|
||||
fi
|
||||
return "$restore_status"
|
||||
}
|
||||
|
||||
wm_cleanup_on_exit() {
|
||||
local status=$?
|
||||
trap - EXIT HUP INT TERM
|
||||
set +e
|
||||
|
||||
if [[ "$transaction_active" == "true" ]]; then
|
||||
wm_log "Unvollständige Routingaktivierung wird zurückgerollt."
|
||||
wm_rollback_transaction
|
||||
fi
|
||||
rm -f "$candidate_file" "$candidate_marker"
|
||||
exit "$status"
|
||||
}
|
||||
|
||||
trap wm_cleanup_on_exit EXIT
|
||||
trap 'exit 130' HUP INT TERM
|
||||
|
||||
# A hard interruption cannot run shell traps. The durable transaction marker
|
||||
# makes the next invocation restore the last validated index before proceeding.
|
||||
if [[ -e "$transaction_file" ]]; then
|
||||
wm_log "Unterbrochene Routingaktualisierung erkannt; letzter gültiger Stand wird wiederhergestellt."
|
||||
if wm_restore_previous_route; then
|
||||
wm_restart_after_restore ||
|
||||
wm_die "Der wiederhergestellte Routingindex bestand den Routentest nicht."
|
||||
else
|
||||
wm_restart_after_restore ||
|
||||
wm_log "Es existierte noch kein vorheriger Routingindex; aktiver Marker wurde entfernt."
|
||||
fi
|
||||
rm -f "$transaction_file"
|
||||
fi
|
||||
|
||||
rm -f "$candidate_file" "$candidate_marker"
|
||||
previous_ready=false
|
||||
if wm_route_data_ready; then
|
||||
previous_ready=true
|
||||
if [[ "$force_rebuild" != "true" ]]; then
|
||||
cp --reflink=auto "$route_file" "$candidate_file"
|
||||
chmod 0644 "$candidate_file"
|
||||
fi
|
||||
fi
|
||||
|
||||
wm_log "Deutschland-/Niederlande-Routingdaten werden in einer Stagingdatei gebaut."
|
||||
if ! wm_compose --profile maintenance run --rm --build \
|
||||
--env "WATERMAPS_LOCAL_FAIRWAYS_PATH=$container_candidate" \
|
||||
--env "WATERMAPS_ROUTE_MARKER_PATH=$container_candidate_marker" \
|
||||
route-data; then
|
||||
wm_die "Der Datenaufbau ist fehlgeschlagen; der aktive Index wurde nicht verändert."
|
||||
fi
|
||||
|
||||
if ! wm_route_data_files_ready "$candidate_file" "$candidate_marker"; then
|
||||
wm_die "Der neu gebaute Routingindex oder sein Bereitschaftsmarker ist ungültig."
|
||||
fi
|
||||
|
||||
if [[ "$previous_ready" == "true" ]] && cmp --silent "$candidate_file" "$route_file"; then
|
||||
wm_log "Geofabrik-Snapshots und Routingindex sind unverändert."
|
||||
rm -f "$candidate_file" "$candidate_marker"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [[ "$previous_ready" == "true" ]]; then
|
||||
wm_copy_route_atomically "$route_file" "$backup_file"
|
||||
wm_write_route_marker "$backup_file" "$backup_marker"
|
||||
wm_route_data_files_ready "$backup_file" "$backup_marker" ||
|
||||
wm_die "Der bisherige Routingindex konnte nicht sicher gesichert werden."
|
||||
else
|
||||
rm -f "$backup_file" "$backup_marker"
|
||||
fi
|
||||
|
||||
temporary_transaction="$(mktemp "$WATERMAPS_RUNTIME_DIR/locks/.route-update-transaction.XXXXXX")"
|
||||
{
|
||||
printf 'started_at=%s\n' "$(date --utc +%Y-%m-%dT%H:%M:%SZ)"
|
||||
printf 'had_previous=%s\n' "$previous_ready"
|
||||
} >"$temporary_transaction"
|
||||
chmod 0644 "$temporary_transaction"
|
||||
mv -f "$temporary_transaction" "$transaction_file"
|
||||
transaction_active=true
|
||||
|
||||
mv -f "$candidate_file" "$route_file"
|
||||
rm -f "$candidate_marker"
|
||||
wm_write_route_marker "$route_file" "$route_marker"
|
||||
wm_route_data_ready ||
|
||||
wm_die "Der aktivierte Routingindex besitzt keinen gültigen Bereitschaftsmarker."
|
||||
|
||||
wm_compose restart watermaps
|
||||
if wm_wait_for_health watermaps 240 && wm_smoke_test_route; then
|
||||
rm -f "$transaction_file"
|
||||
transaction_active=false
|
||||
rm -f "$backup_file" "$backup_marker"
|
||||
wm_log "Routingdaten-Update wurde erfolgreich aktiviert."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
wm_log "Der neue Routingindex bestand die Routentests nicht; Rollback wird ausgeführt."
|
||||
if wm_rollback_transaction; then
|
||||
rm -f "$backup_file" "$backup_marker"
|
||||
wm_die "Neuer Routingindex verworfen; der vorherige, erneut getestete Index ist aktiv."
|
||||
fi
|
||||
|
||||
if [[ "$rollback_restored_previous" == "true" ]]; then
|
||||
wm_die "Rollback-Dateien wurden wiederhergestellt, bestanden aber den Routentest nicht."
|
||||
fi
|
||||
wm_die "Neuer Routingindex verworfen; es existierte noch kein vorheriger gültiger Index."
|
||||
Executable
+131
@@ -0,0 +1,131 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -Eeuo pipefail
|
||||
|
||||
WM_DEPLOY_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
WM_ROOT_DIR="$(cd "$WM_DEPLOY_DIR/.." && pwd)"
|
||||
# shellcheck source=remote-common.sh
|
||||
source "$WM_DEPLOY_DIR/scripts/remote-common.sh"
|
||||
|
||||
server_ipv4=""
|
||||
identity_file=""
|
||||
run_go_live=false
|
||||
|
||||
usage() {
|
||||
cat <<'USAGE'
|
||||
Verwendung:
|
||||
./deploy/scripts/upload-and-deploy.sh [Optionen]
|
||||
|
||||
Optionen:
|
||||
--host IPV4 Server-IP statt `tofu output -raw server_ipv4`
|
||||
--identity DATEI Privater SSH-Key (alternativ WATERMAPS_SSH_KEY)
|
||||
--user BENUTZER SSH-Benutzer; Standard: deploy
|
||||
--go-live Nach dem Deployment sofort den SSL-Livegang versuchen
|
||||
-h, --help Hilfe anzeigen
|
||||
|
||||
Die Datei deploy/.env.production muss lokal vorhanden sein. Sie wird separat
|
||||
mit Dateimodus 0600 übertragen. Terraform-State, .terraform, data,
|
||||
node_modules, lokale Env-Dateien und Git-Metadaten werden ausgeschlossen.
|
||||
USAGE
|
||||
}
|
||||
|
||||
while [[ "$#" -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--host)
|
||||
[[ "$#" -ge 2 ]] || wm_local_die "Wert für --host fehlt."
|
||||
server_ipv4="$2"
|
||||
shift 2
|
||||
;;
|
||||
--identity)
|
||||
[[ "$#" -ge 2 ]] || wm_local_die "Wert für --identity fehlt."
|
||||
identity_file="$2"
|
||||
shift 2
|
||||
;;
|
||||
--user)
|
||||
[[ "$#" -ge 2 ]] || wm_local_die "Wert für --user fehlt."
|
||||
WATERMAPS_SSH_USER="$2"
|
||||
export WATERMAPS_SSH_USER
|
||||
shift 2
|
||||
;;
|
||||
--go-live)
|
||||
run_go_live=true
|
||||
shift
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
wm_local_die "Unbekannte Option: $1"
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
command -v rsync >/dev/null 2>&1 || wm_local_die "rsync ist lokal nicht installiert."
|
||||
local_env="$WM_DEPLOY_DIR/.env.production"
|
||||
[[ -f "$local_env" ]] ||
|
||||
wm_local_die "Bitte zuerst deploy/.env.production aus der Vorlage erstellen und konfigurieren."
|
||||
|
||||
wm_local_resolve_ssh "$server_ipv4" "$identity_file"
|
||||
wm_local_wait_for_ssh
|
||||
|
||||
remote_prefix=""
|
||||
rsync_path="rsync"
|
||||
if [[ -n "$WM_REMOTE_SUDO" ]]; then
|
||||
remote_prefix="$WM_REMOTE_SUDO "
|
||||
rsync_path="$WM_REMOTE_SUDO rsync"
|
||||
fi
|
||||
|
||||
printf '[watermaps] Warte auf Cloud-init und das persistente Hetzner-Volume.\n'
|
||||
ssh "${WM_SSH_OPTIONS[@]}" "$WM_SSH_TARGET" \
|
||||
"cloud-init status --wait && ${remote_prefix}systemctl start watermaps-volume-setup.service && mountpoint --quiet /srv/watermaps-data"
|
||||
|
||||
printf '[watermaps] Übertrage Projekt nach %s:/opt/watermaps\n' "$WM_SSH_TARGET"
|
||||
ssh "${WM_SSH_OPTIONS[@]}" "$WM_SSH_TARGET" \
|
||||
"${remote_prefix}install -d -m 0755 /opt/watermaps /opt/watermaps/deploy"
|
||||
|
||||
rsync \
|
||||
--archive \
|
||||
--compress \
|
||||
--delete-delay \
|
||||
--human-readable \
|
||||
--rsync-path="$rsync_path" \
|
||||
--exclude='.git/' \
|
||||
--exclude='.terraform/' \
|
||||
--exclude='.terraform.tfstate.lock.info' \
|
||||
--exclude='*.tfvars' \
|
||||
--exclude='*.tfvars.json' \
|
||||
--exclude='*.tfplan' \
|
||||
--exclude='*.tfstate*' \
|
||||
--exclude='crash.log' \
|
||||
--exclude='crash.*.log' \
|
||||
--exclude='/data/' \
|
||||
--exclude='/.tools/' \
|
||||
--exclude='node_modules/' \
|
||||
--exclude='**/.env' \
|
||||
--exclude='**/.env.*' \
|
||||
--exclude='/deploy/.env.production' \
|
||||
-e "ssh ${WM_SSH_OPTIONS[*]@Q}" \
|
||||
"$WM_ROOT_DIR/" \
|
||||
"$WM_SSH_TARGET:/opt/watermaps/"
|
||||
|
||||
rsync \
|
||||
--archive \
|
||||
--chmod=F600 \
|
||||
--rsync-path="$rsync_path" \
|
||||
-e "ssh ${WM_SSH_OPTIONS[*]@Q}" \
|
||||
"$local_env" \
|
||||
"$WM_SSH_TARGET:/opt/watermaps/deploy/.env.production"
|
||||
|
||||
remote_command="${remote_prefix}chmod +x /opt/watermaps/deploy/scripts/*.sh"
|
||||
remote_command+=" && ${remote_prefix}/opt/watermaps/deploy/scripts/bootstrap-server.sh"
|
||||
remote_command+=" && ${remote_prefix}/opt/watermaps/deploy/scripts/deploy.sh"
|
||||
ssh "${WM_SSH_OPTIONS[@]}" "$WM_SSH_TARGET" "$remote_command"
|
||||
|
||||
if [[ "$run_go_live" == "true" ]]; then
|
||||
ssh "${WM_SSH_OPTIONS[@]}" "$WM_SSH_TARGET" \
|
||||
"${remote_prefix}/opt/watermaps/deploy/scripts/go-live.sh '$WM_SERVER_IPV4'"
|
||||
else
|
||||
printf '[watermaps] Deployment abgeschlossen. Nach dem DNS-Eintrag:\n'
|
||||
printf ' ./deploy/scripts/remote-go-live.sh --host %s\n' "$WM_SERVER_IPV4"
|
||||
fi
|
||||
@@ -0,0 +1,11 @@
|
||||
[Unit]
|
||||
Description=Watermaps Let's-Encrypt-Zertifikat erneuern
|
||||
Wants=network-online.target
|
||||
After=network-online.target docker.service watermaps-volume-setup.service
|
||||
Requires=docker.service
|
||||
RequiresMountsFor=/srv/watermaps-data
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
WorkingDirectory=/opt/watermaps
|
||||
ExecStart=/opt/watermaps/deploy/scripts/renew-certificate.sh
|
||||
@@ -0,0 +1,11 @@
|
||||
[Unit]
|
||||
Description=Watermaps-Zertifikat zweimal täglich prüfen
|
||||
|
||||
[Timer]
|
||||
OnCalendar=*-*-* 03,15:17:00
|
||||
RandomizedDelaySec=45m
|
||||
Persistent=true
|
||||
Unit=watermaps-certbot-renew.service
|
||||
|
||||
[Install]
|
||||
WantedBy=timers.target
|
||||
@@ -0,0 +1,14 @@
|
||||
[Unit]
|
||||
Description=Watermaps Deutschland-/Niederlande-Routingdaten aktualisieren
|
||||
Wants=network-online.target
|
||||
After=network-online.target docker.service watermaps-volume-setup.service
|
||||
Requires=docker.service
|
||||
RequiresMountsFor=/srv/watermaps-data
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
WorkingDirectory=/opt/watermaps
|
||||
ExecStart=/opt/watermaps/deploy/scripts/update-route-data.sh
|
||||
Nice=10
|
||||
IOSchedulingClass=best-effort
|
||||
IOSchedulingPriority=7
|
||||
@@ -0,0 +1,11 @@
|
||||
[Unit]
|
||||
Description=Täglich Watermaps-Routingdaten aktualisieren
|
||||
|
||||
[Timer]
|
||||
OnCalendar=*-*-* 06:15:00
|
||||
RandomizedDelaySec=30m
|
||||
Persistent=true
|
||||
Unit=watermaps-route-update.service
|
||||
|
||||
[Install]
|
||||
WantedBy=timers.target
|
||||
Reference in New Issue
Block a user