#!/usr/bin/env bash set -Eeuo pipefail umask 077 TARGET_VERSION="0.0.104" ARTIFACT_BASE_URL="https://updates.putex.org/ristu/core/releases/0.0.104" UPDATE_MANIFEST_URL="https://updates.putex.org/ristu/core/stable.json" EXPECTED_COMPOSE_SHA256="0967a44f30ae74ec1cd75c99e312839f752fa10a45093d601e396df2cd433047" EXPECTED_ENV_SHA256="f081a9953c0adbded3f0f963dc8e35eb6d7bcad0f313f28f58b6c5df861a0a05" log() { printf '%s\n' "Ristu: $*" } fail() { printf '%s\n' "Ristu: błąd: $*" >&2 exit 1 } command -v curl >/dev/null 2>&1 || fail "brak programu curl" TARGET_DIR="$(pwd -P)" ENV_FILE="$TARGET_DIR/.env" COMPOSE_FILE="$TARGET_DIR/docker-compose.yml" ENV_EXAMPLE_FILE="$TARGET_DIR/.env.example" if [[ -f "$ENV_FILE" ]]; then MODE="update" elif [[ -e "$ENV_FILE" ]]; then fail "plik .env istnieje, ale nie jest zwykłym plikiem" else shopt -s nullglob dotglob EXISTING_ENTRIES=("$TARGET_DIR"/*) shopt -u nullglob dotglob [[ "${#EXISTING_ENTRIES[@]}" -eq 0 ]] || fail "nową instalację uruchom w całkowicie pustym katalogu; istniejącą instalację uruchom w katalogu zawierającym plik .env" MODE="install" fi command -v docker >/dev/null 2>&1 || fail "brak programu Docker" docker compose version >/dev/null 2>&1 || fail "brak Docker Compose v2 (polecenie: docker compose)" UP_HELP="$(docker compose up --help 2>&1)" || fail "nie można sprawdzić możliwości Docker Compose" [[ "$UP_HELP" == *"--wait"* ]] || fail "Docker Compose jest zbyt stary; zaktualizuj go do wersji obsługującej --wait" contains_known_container() { local name while IFS= read -r name; do case "$name" in ristu-db|orthanc-db|orthanc-auth-service|orthanc|ohif|ristu-api|ristu-web) return 0 ;; esac done return 1 } contains_known_volume() { local name while IFS= read -r name; do case "$name" in ristu-fresh_ristu_db|ristu-fresh_ristu_api_runtime|ristu-fresh_ristu_web_tls|ristu-fresh_orthanc_db|ristu-fresh_orthanc_dicom) return 0 ;; esac done return 1 } if [[ "$MODE" == "install" ]]; then EXISTING_CONTAINERS="$(docker ps -a --format '{{.Names}}')" || fail "nie można sprawdzić istniejących kontenerów" contains_known_container <<< "$EXISTING_CONTAINERS" && fail "Docker zawiera już kontenery Ristu; odzyskaj ich plik .env i uruchom tryb aktualizacji" EXISTING_VOLUMES="$(docker volume ls --format '{{.Name}}')" || fail "nie można sprawdzić istniejących wolumenów" contains_known_volume <<< "$EXISTING_VOLUMES" && fail "Docker zawiera już wolumeny Ristu; nie można bezpiecznie utworzyć nowych sekretów instalacji" fi TEMP_DIR="$(mktemp -d "$TARGET_DIR/.ristu-bootstrap.XXXXXX")" cleanup() { if [[ -n "${TEMP_DIR:-}" && -d "$TEMP_DIR" && "$TEMP_DIR" == "$TARGET_DIR"/.ristu-bootstrap.* ]]; then rm -rf -- "$TEMP_DIR" fi } trap cleanup EXIT CANDIDATE_COMPOSE="$TEMP_DIR/docker-compose.yml" CANDIDATE_ENV_EXAMPLE="$TEMP_DIR/.env.example" CANDIDATE_ENV="$TEMP_DIR/.env" download() { local url="$1" local output="$2" curl --proto '=https' --tlsv1.2 --fail --silent --show-error --location \ --connect-timeout 15 --max-time 120 --output "$output" "$url" } sha256_file() { local path="$1" if command -v sha256sum >/dev/null 2>&1; then sha256sum "$path" | awk '{print $1}' elif command -v shasum >/dev/null 2>&1; then shasum -a 256 "$path" | awk '{print $1}' elif command -v openssl >/dev/null 2>&1; then openssl dgst -sha256 "$path" | awk '{print $NF}' else fail "brak narzędzia do sprawdzania SHA-256" fi } verify_sha256() { local path="$1" local expected="$2" local actual actual="$(sha256_file "$path")" [[ "$actual" == "$expected" ]] || fail "pobrany plik $(basename "$path") ma nieprawidłową sumę SHA-256" } INSTALL_SECRET_KEYS=( RISTU_DB_PASSWORD ORTHANC_DB_PASSWORD ORTHANC_AUTH_SERVICE_PASSWORD ORTHANC_AUTH_SECRET RISTU_PROXY_SESSION_SECRET RISTU_SETTINGS_SECRET ) is_install_secret_key() { case "$1" in RISTU_DB_PASSWORD|ORTHANC_DB_PASSWORD|ORTHANC_AUTH_SERVICE_PASSWORD|ORTHANC_AUTH_SECRET|RISTU_PROXY_SESSION_SECRET|RISTU_SETTINGS_SECRET) return 0 ;; *) return 1 ;; esac } random_secret() { local secret if command -v openssl >/dev/null 2>&1; then secret="$(openssl rand -hex 32)" || fail "nie można wygenerować sekretów instalacji" elif command -v od >/dev/null 2>&1 && [[ -r /dev/urandom ]]; then secret="$(LC_ALL=C od -An -N32 -tx1 /dev/urandom | tr -d '[:space:]')" || fail "nie można wygenerować sekretów instalacji" else fail "brak bezpiecznego generatora sekretów (openssl albo od i /dev/urandom)" fi [[ "$secret" =~ ^[0-9a-f]{64}$ ]] || fail "generator sekretów zwrócił nieprawidłową wartość" printf '%s' "$secret" } if [[ "$MODE" == "install" ]]; then log "tryb nowej instalacji w pustym katalogu" else log "tryb aktualizacji istniejącej instalacji" fi log "pobieranie plików wydania $TARGET_VERSION" download "$ARTIFACT_BASE_URL/docker-compose.yml" "$CANDIDATE_COMPOSE" download "$ARTIFACT_BASE_URL/env.example" "$CANDIDATE_ENV_EXAMPLE" verify_sha256 "$CANDIDATE_COMPOSE" "$EXPECTED_COMPOSE_SHA256" verify_sha256 "$CANDIDATE_ENV_EXAMPLE" "$EXPECTED_ENV_SHA256" if [[ "$MODE" == "install" ]]; then : > "$CANDIDATE_ENV" while IFS= read -r line || [[ -n "$line" ]]; do line="${line%$'\r'}" if [[ "$line" =~ ^([A-Za-z_][A-Za-z0-9_]*)= ]]; then key="${BASH_REMATCH[1]}" if is_install_secret_key "$key"; then printf '%s=%s\n' "$key" "$(random_secret)" >> "$CANDIDATE_ENV" continue fi if [[ "$key" == "RISTU_IMAGE_TAG" ]]; then printf '%s\n' "RISTU_IMAGE_TAG=latest" >> "$CANDIDATE_ENV" continue fi fi printf '%s\n' "$line" >> "$CANDIDATE_ENV" done < "$CANDIDATE_ENV_EXAMPLE" for key in "${INSTALL_SECRET_KEYS[@]}"; do [[ "$(grep -Ec "^${key}=" "$CANDIDATE_ENV")" -eq 1 ]] || fail "env.example nie zawiera dokładnie jednego pola $key" done ! grep -Eq '^[A-Za-z_][A-Za-z0-9_]*=.*HASLO_ZMIEN_MNIE' "$CANDIDATE_ENV" || fail "nie zastąpiono wszystkich placeholderów sekretów" else # Nie wykonujemy .env jako kodu. Zachowujemy sekrety i własny, niepusty # adres manifestu, uzupełniamy pusty adres oficjalnym kanałem, ustawiamy # ruchomy tag obrazów i dopisujemy brakujące klucze z aktualnego przykładu. awk -v update_manifest_url="$UPDATE_MANIFEST_URL" ' BEGIN { image_tag_written = 0; manifest_url_written = 0 } { sub(/\r$/, "") } /^[[:space:]]*RISTU_UPDATE_MANIFEST_URL[[:space:]]*=/ { if (!manifest_url_written) { value = $0 sub(/^[^=]*=/, "", value) if (value ~ /^[[:space:]]*$/) { print "RISTU_UPDATE_MANIFEST_URL=" update_manifest_url } else { print $0 } manifest_url_written = 1 } next } /^[[:space:]]*RISTU_IMAGE_TAG[[:space:]]*=/ { if (!image_tag_written) { print "RISTU_IMAGE_TAG=latest" image_tag_written = 1 } next } { print } END { if (!image_tag_written) print "RISTU_IMAGE_TAG=latest" } ' "$ENV_FILE" > "$CANDIDATE_ENV" missing_header_written=0 while IFS= read -r line || [[ -n "$line" ]]; do line="${line%$'\r'}" if [[ "$line" =~ ^([A-Za-z_][A-Za-z0-9_]*)= ]]; then key="${BASH_REMATCH[1]}" if ! grep -Eq "^[[:space:]]*${key}[[:space:]]*=" "$CANDIDATE_ENV"; then if [[ "$missing_header_written" -eq 0 ]]; then printf '\n# Ustawienia dodane automatycznie podczas aktualizacji Ristu.\n' >> "$CANDIDATE_ENV" missing_header_written=1 fi if is_install_secret_key "$key"; then printf '%s=%s\n' "$key" "$(random_secret)" >> "$CANDIDATE_ENV" else printf '%s\n' "$line" >> "$CANDIDATE_ENV" fi fi fi done < "$CANDIDATE_ENV_EXAMPLE" fi CANDIDATE_FILES=(--env-file "$CANDIDATE_ENV" -f "$CANDIDATE_COMPOSE") FINAL_FILES=(--env-file "$ENV_FILE" -f "$COMPOSE_FILE") if [[ -f "$TARGET_DIR/docker-compose.override.yml" ]]; then CANDIDATE_FILES+=(-f "$TARGET_DIR/docker-compose.override.yml") FINAL_FILES+=(-f "$TARGET_DIR/docker-compose.override.yml") fi log "sprawdzanie konfiguracji" docker compose "${CANDIDATE_FILES[@]}" config --quiet log "pobieranie najnowszych obrazów" docker compose "${CANDIDATE_FILES[@]}" pull if [[ "$MODE" == "update" ]]; then BACKUP_ROOT="$TARGET_DIR/.ristu-backups" BACKUP_DIR="$BACKUP_ROOT/$(date -u +%Y%m%dT%H%M%SZ)-$$" mkdir -p "$BACKUP_ROOT" mkdir "$BACKUP_DIR" cp -p "$ENV_FILE" "$BACKUP_DIR/.env" [[ ! -f "$COMPOSE_FILE" ]] || cp -p "$COMPOSE_FILE" "$BACKUP_DIR/docker-compose.yml" [[ ! -f "$ENV_EXAMPLE_FILE" ]] || cp -p "$ENV_EXAMPLE_FILE" "$BACKUP_DIR/.env.example" fi mv -f "$CANDIDATE_COMPOSE" "$COMPOSE_FILE" mv -f "$CANDIDATE_ENV_EXAMPLE" "$ENV_EXAMPLE_FILE" mv -f "$CANDIDATE_ENV" "$ENV_FILE" chmod 600 "$ENV_FILE" chmod 644 "$COMPOSE_FILE" "$ENV_EXAMPLE_FILE" log "uruchamianie wydania z obrazów latest" docker compose "${FINAL_FILES[@]}" up -d --remove-orphans --wait docker compose "${FINAL_FILES[@]}" ps if [[ "$MODE" == "install" ]]; then log "instalacja zakończona" else log "aktualizacja zakończona; kopia poprzedniej konfiguracji: $BACKUP_DIR" fi