Files
watermaps/deploy/scripts/common.sh
T

364 lines
12 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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: "EmdenDitzum",
start: { lat: 53.3422, lon: 7.1871 },
destination: { lat: 53.465, lon: 7.4734 },
minimumAlternatives: 2
},
{
name: "NorddeichNorderney",
start: { lat: 53.6234, lon: 7.1559 },
destination: { lat: 53.7023, lon: 7.1658 },
minimumAlternatives: 2
},
{
name: "EmdenDelfzijl",
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: "WeespUtrecht",
start: { lat: 52.309, lon: 5.0423 },
destination: { lat: 52.105, lon: 5.085 },
minimumAlternatives: 2
},
{
name: "LemmerSneek",
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);
}
}
'
}