From 1bdcedefdc6e2c556de13a3d253a9329c51f21ce Mon Sep 17 00:00:00 2001 From: suphicakir Date: Sat, 7 Nov 2026 02:13:10 +0300 Subject: [PATCH] =?UTF-8?q?feat(ops):=20duman=20testi,=20yedekleme=20ve=20?= =?UTF-8?q?geri=20y=C3=BCkleme=20betikleri?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - smoke-test.sh: sağlık, replica set, güvenlik başlıkları, HTTPS, oturumsuz erişim, giriş - butcem-backup.sh: mongodump + uploads arşivi, SHA-256, 7 gün saklama - butcem-restore.sh: bütünlük kontrolü ve onayla geri yükleme Co-Authored-By: Claude Opus 5.5 --- scripts/backup/butcem-backup.env.example | 13 +++++ scripts/backup/butcem-backup.sh | 66 ++++++++++++++++++++++ scripts/backup/butcem-restore.sh | 52 +++++++++++++++++ scripts/smoke-test.sh | 72 ++++++++++++++++++++++++ 4 files changed, 203 insertions(+) create mode 100644 scripts/backup/butcem-backup.env.example create mode 100755 scripts/backup/butcem-backup.sh create mode 100755 scripts/backup/butcem-restore.sh create mode 100755 scripts/smoke-test.sh diff --git a/scripts/backup/butcem-backup.env.example b/scripts/backup/butcem-backup.env.example new file mode 100644 index 0000000..8f23148 --- /dev/null +++ b/scripts/backup/butcem-backup.env.example @@ -0,0 +1,13 @@ +# /etc/butcem-backup.env — butcem-backup.sh ayarları. İzin: sudo chmod 600 /etc/butcem-backup.env +# Değerler için docs/DEPLOY.md › Yedekleme. + +# MongoDB konteyner adı veya adının ayırt edici bir parçası (docker ps --format '{{.Names}}') +MONGO_CONTAINER="butcem-mongo" +MONGO_ROOT_USER="butcem_root" +MONGO_ROOT_PASSWORD="" + +# Uygulamanın /app/uploads volume adı (docker volume ls) +UPLOADS_VOLUME="butcem-uploads" + +BACKUP_DIR="/var/backups/butcem" +RETENTION_DAYS="7" diff --git a/scripts/backup/butcem-backup.sh b/scripts/backup/butcem-backup.sh new file mode 100755 index 0000000..08f1bc8 --- /dev/null +++ b/scripts/backup/butcem-backup.sh @@ -0,0 +1,66 @@ +#!/usr/bin/env bash +# BÜTÇEM günlük yedek — sunucuda (Atlas) root olarak, cron ile çalışır. Ayrıntı: docs/DEPLOY.md › Yedekleme +# +# Üretir (BACKUP_DIR altında, tarih damgalı): +# mongo-YYYYmmdd-HHMMSS.archive.gz → butcem veritabanı (mongodump --archive --gzip) +# uploads-YYYYmmdd-HHMMSS.tar.gz → yüklenen dosyalar (uploads volume) +# SHA256SUMS-YYYYmmdd-HHMMSS → bütünlük özetleri +# RETENTION_DAYS günden eski yedekleri siler. +# +# Ayarlar /etc/butcem-backup.env dosyasından okunur (yalnızca root okuyabilmeli: chmod 600). +set -euo pipefail + +CONFIG="${BUTCEM_BACKUP_CONFIG:-/etc/butcem-backup.env}" +# shellcheck source=/dev/null +[ -f "$CONFIG" ] && . "$CONFIG" + +: "${MONGO_CONTAINER:?MONGO_CONTAINER tanımlı değil (MongoDB konteyner adı veya adının bir parçası)}" +: "${MONGO_ROOT_USER:?MONGO_ROOT_USER tanımlı değil}" +: "${MONGO_ROOT_PASSWORD:?MONGO_ROOT_PASSWORD tanımlı değil}" +: "${UPLOADS_VOLUME:?UPLOADS_VOLUME tanımlı değil (uygulamanın /app/uploads volume adı)}" +BACKUP_DIR="${BACKUP_DIR:-/var/backups/butcem}" +RETENTION_DAYS="${RETENTION_DAYS:-7}" +DB_NAME="${DB_NAME:-butcem}" +HELPER_IMAGE="${HELPER_IMAGE:-alpine:3.20}" + +TS="$(date +%Y%m%d-%H%M%S)" +log() { echo "[$(date '+%F %T')] $*"; } + +# Dokploy konteyner adlarına rastgele son ek ekler; adın bir parçasıyla eşleşen çalışan konteyner. +CID="$(docker ps -q --filter "name=${MONGO_CONTAINER}" | head -n1)" +[ -n "$CID" ] || { log "HATA: '${MONGO_CONTAINER}' adlı çalışan MongoDB konteyneri yok."; exit 1; } +docker volume inspect "$UPLOADS_VOLUME" >/dev/null 2>&1 || { log "HATA: '${UPLOADS_VOLUME}' volume'u yok."; exit 1; } + +install -d -m 700 "$BACKUP_DIR" +MONGO_FILE="$BACKUP_DIR/mongo-$TS.archive.gz" +UPLOADS_FILE="$BACKUP_DIR/uploads-$TS.tar.gz" + +# 1) MongoDB: parola komut satırında görünmesin diye geçici yapılandırma dosyasıyla. +log "MongoDB yedeği alınıyor ($DB_NAME)…" +CONF="/tmp/butcem-dump-$TS.yaml" +printf 'password: "%s"\n' "$MONGO_ROOT_PASSWORD" | docker exec -i "$CID" sh -c "umask 077 && cat > $CONF" +if ! docker exec "$CID" mongodump --quiet --config "$CONF" --username "$MONGO_ROOT_USER" \ + --authenticationDatabase admin --db "$DB_NAME" --archive --gzip >"$MONGO_FILE"; then + docker exec "$CID" rm -f "$CONF" + rm -f "$MONGO_FILE" + log "HATA: mongodump başarısız." + exit 1 +fi +docker exec "$CID" rm -f "$CONF" +[ -s "$MONGO_FILE" ] || { log "HATA: MongoDB yedeği boş."; exit 1; } + +# 2) Yüklenen dosyalar: volume salt okunur bağlanır. +log "Uploads yedeği alınıyor ($UPLOADS_VOLUME)…" +docker run --rm -v "$UPLOADS_VOLUME":/data:ro -v "$BACKUP_DIR":/backup "$HELPER_IMAGE" \ + tar czf "/backup/$(basename "$UPLOADS_FILE")" -C /data . +[ -s "$UPLOADS_FILE" ] || { log "HATA: uploads yedeği boş."; exit 1; } + +# 3) Bütünlük özeti ve izinler +( cd "$BACKUP_DIR" && sha256sum "$(basename "$MONGO_FILE")" "$(basename "$UPLOADS_FILE")" >"SHA256SUMS-$TS" ) +chmod 600 "$MONGO_FILE" "$UPLOADS_FILE" "$BACKUP_DIR/SHA256SUMS-$TS" + +# 4) Saklama süresi +find "$BACKUP_DIR" -maxdepth 1 -type f \( -name 'mongo-*.archive.gz' -o -name 'uploads-*.tar.gz' -o -name 'SHA256SUMS-*' \) \ + -mtime +"$RETENTION_DAYS" -print -delete | sed 's/^/ silindi: /' + +log "Tamam: $(du -h "$MONGO_FILE" | cut -f1) veritabanı, $(du -h "$UPLOADS_FILE" | cut -f1) dosya → $BACKUP_DIR" diff --git a/scripts/backup/butcem-restore.sh b/scripts/backup/butcem-restore.sh new file mode 100755 index 0000000..ee37ccb --- /dev/null +++ b/scripts/backup/butcem-restore.sh @@ -0,0 +1,52 @@ +#!/usr/bin/env bash +# BÜTÇEM geri yükleme — sunucuda root olarak, ELLE çalıştırılır. Ayrıntı: docs/DEPLOY.md › Geri yükleme +# +# sudo scripts/backup/butcem-restore.sh /var/backups/butcem/mongo-….archive.gz [/var/backups/butcem/uploads-….tar.gz] +# +# DİKKAT: butcem veritabanındaki koleksiyonlar yedektekilerle DEĞİŞTİRİLİR (--drop); uploads arşivi +# verilirse volume içeriği silinip arşivden yazılır. Önce uygulamayı durdurun (Dokploy › Stop). +# Ayarlar butcem-backup.sh ile aynı dosyadan: /etc/butcem-backup.env +set -euo pipefail + +CONFIG="${BUTCEM_BACKUP_CONFIG:-/etc/butcem-backup.env}" +# shellcheck source=/dev/null +[ -f "$CONFIG" ] && . "$CONFIG" +: "${MONGO_CONTAINER:?}" "${MONGO_ROOT_USER:?}" "${MONGO_ROOT_PASSWORD:?}" "${UPLOADS_VOLUME:?}" +DB_NAME="${DB_NAME:-butcem}" +HELPER_IMAGE="${HELPER_IMAGE:-alpine:3.20}" + +MONGO_FILE="${1:?Kullanım: butcem-restore.sh [uploads-arşivi]}" +UPLOADS_FILE="${2:-}" +[ -s "$MONGO_FILE" ] || { echo "Arşiv yok veya boş: $MONGO_FILE"; exit 1; } +[ -z "$UPLOADS_FILE" ] || [ -s "$UPLOADS_FILE" ] || { echo "Arşiv yok veya boş: $UPLOADS_FILE"; exit 1; } + +# Bütünlük kontrolü (aynı klasörde SHA256SUMS varsa) +DIR="$(cd "$(dirname "$MONGO_FILE")" && pwd)" +STAMP="$(basename "$MONGO_FILE" | sed -E 's/^mongo-(.*)\.archive\.gz$/\1/')" +if [ -f "$DIR/SHA256SUMS-$STAMP" ]; then + (cd "$DIR" && sha256sum -c "SHA256SUMS-$STAMP") || { echo "Bütünlük kontrolü başarısız."; exit 1; } +fi + +CID="$(docker ps -q --filter "name=${MONGO_CONTAINER}" | head -n1)" +[ -n "$CID" ] || { echo "Çalışan MongoDB konteyneri yok: $MONGO_CONTAINER"; exit 1; } + +echo "Geri yüklenecek: $MONGO_FILE${UPLOADS_FILE:+ ve $UPLOADS_FILE}" +echo "'$DB_NAME' veritabanındaki mevcut veri yedektekiyle DEĞİŞTİRİLECEK." +if [ "${ASSUME_YES:-0}" != "1" ]; then + read -r -p "Devam etmek için 'evet' yazın: " ANSWER + [ "$ANSWER" = "evet" ] || { echo "Vazgeçildi."; exit 1; } +fi + +CONF="/tmp/butcem-restore-$$.yaml" +printf 'password: "%s"\n' "$MONGO_ROOT_PASSWORD" | docker exec -i "$CID" sh -c "umask 077 && cat > $CONF" +trap 'docker exec "$CID" rm -f "$CONF" >/dev/null 2>&1 || true' EXIT +docker exec -i "$CID" mongorestore --quiet --config "$CONF" --username "$MONGO_ROOT_USER" \ + --authenticationDatabase admin --archive --gzip --drop --nsInclude "${DB_NAME}.*" <"$MONGO_FILE" +echo "✓ Veritabanı geri yüklendi." + +if [ -n "$UPLOADS_FILE" ]; then + docker run --rm -v "$UPLOADS_VOLUME":/data -v "$(cd "$(dirname "$UPLOADS_FILE")" && pwd)":/backup:ro "$HELPER_IMAGE" \ + sh -c "find /data -mindepth 1 -delete && tar xzf /backup/$(basename "$UPLOADS_FILE") -C /data && chown -R 1001:1001 /data" + echo "✓ Yüklenen dosyalar geri yüklendi." +fi +echo "Uygulamayı yeniden başlatın (Dokploy › Start) ve duman testini çalıştırın." diff --git a/scripts/smoke-test.sh b/scripts/smoke-test.sh new file mode 100755 index 0000000..b71b318 --- /dev/null +++ b/scripts/smoke-test.sh @@ -0,0 +1,72 @@ +#!/usr/bin/env bash +# BÜTÇEM duman testi (smoke test) — canlıya alma sonrası, yalnızca OKUMA yapar; veri değiştirmez. +# +# Kullanım: +# scripts/smoke-test.sh https://butcem.ta1sph.website [beklenen-sürüm] +# SMOKE_USER=admin SMOKE_PASSWORD='…' scripts/smoke-test.sh https://butcem.ta1sph.website 1.0.0 +# +# Gerekenler: bash, curl. Çıkış kodu: 0 = hepsi geçti, 1 = en az bir kontrol başarısız. +set -uo pipefail + +BASE="${1:?Kullanım: scripts/smoke-test.sh [beklenen-sürüm]}" +BASE="${BASE%/}" +EXPECT_VERSION="${2:-}" +FAIL=0 +TMP="$(mktemp -d)" +trap 'rm -rf "$TMP"' EXIT + +ok() { printf ' \033[32m✓\033[0m %s\n' "$1"; } +bad() { printf ' \033[31m✗\033[0m %s\n' "$1"; FAIL=1; } +check() { if eval "$2"; then ok "$1"; else bad "$1"; fi; } + +echo "BÜTÇEM duman testi: $BASE" + +echo "1) Sağlık kontrolü" +HEALTH_CODE=$(curl -s -o "$TMP/health" -w '%{http_code}' "$BASE/api/health") +HEALTH=$(cat "$TMP/health") +echo " $HEALTH" +check "GET /api/health 200" '[ "$HEALTH_CODE" = 200 ]' +check "veritabanı erişilebilir" 'grep -q "\"db\":\"ok\"" <<<"$HEALTH"' +check "MongoDB replica set (transaction için)" '! grep -q "\"replicaSet\":null" <<<"$HEALTH" && grep -q "\"replicaSet\":\"" <<<"$HEALTH"' +if [ -n "$EXPECT_VERSION" ]; then + check "sürüm $EXPECT_VERSION" 'grep -q "\"version\":\"$EXPECT_VERSION\"" <<<"$HEALTH"' +fi + +echo "2) Sayfalar ve güvenlik başlıkları" +LOGIN_CODE=$(curl -s -D "$TMP/login-headers" -o /dev/null -w '%{http_code}' "$BASE/login") +H=$(tr -d '\r' <"$TMP/login-headers" | tr 'A-Z' 'a-z') +check "GET /login 200" '[ "$LOGIN_CODE" = 200 ]' +check "Content-Security-Policy var" 'grep -q "^content-security-policy:" <<<"$H"' +check "X-Frame-Options: DENY" 'grep -q "^x-frame-options: deny" <<<"$H"' +check "X-Content-Type-Options: nosniff" 'grep -q "^x-content-type-options: nosniff" <<<"$H"' +if [[ "$BASE" == https://* ]]; then + check "Strict-Transport-Security var" 'grep -q "^strict-transport-security:" <<<"$H"' + HTTP_URL="http://${BASE#https://}/login" + HTTP_CODE=$(curl -s -o /dev/null -w '%{http_code}' "$HTTP_URL") + check "HTTP → HTTPS yönlendirmesi ($HTTP_CODE)" '[[ "$HTTP_CODE" =~ ^30[1278]$ ]]' +fi + +echo "3) Oturumsuz erişim engelleniyor" +DASH=$(curl -s -o /dev/null -w '%{http_code} %{redirect_url}' "$BASE/dashboard") +check "/dashboard → giriş sayfasına yönlendirme ($DASH)" '[[ "$DASH" =~ ^30[27]\ .*/login ]]' +FILE_CODE=$(curl -s -o /dev/null -w '%{http_code}' "$BASE/api/files/000000000000000000000000") +check "/api/files oturumsuz 401 ($FILE_CODE)" '[ "$FILE_CODE" = 401 ]' +CSV=$(curl -s -o "$TMP/csv" -w '%{http_code}' "$BASE/raporlar/kategoriler/csv") +check "rapor CSV'si oturumsuz veri vermiyor ($CSV)" '! grep -q "Toplam gidere oran" "$TMP/csv"' + +if [ -n "${SMOKE_USER:-}" ] && [ -n "${SMOKE_PASSWORD:-}" ]; then + echo "4) Giriş ($SMOKE_USER)" + SIGN=$(curl -s -c "$TMP/jar" -o "$TMP/signin" -w '%{http_code}' -X POST "$BASE/api/auth/sign-in/username" \ + -H 'Content-Type: application/json' -H "Origin: $BASE" \ + --data "$(printf '{"username":"%s","password":"%s"}' "$SMOKE_USER" "$SMOKE_PASSWORD")") + check "giriş 200 ($SIGN)" '[ "$SIGN" = 200 ]' + PAGE=$(curl -s -b "$TMP/jar" -L -o /dev/null -w '%{http_code} %{url_effective}' "$BASE/dashboard") + check "oturumla ana sayfa açılıyor ($PAGE)" '[[ "$PAGE" =~ ^200\ ]] && [[ ! "$PAGE" =~ /login ]]' + curl -s -b "$TMP/jar" -X POST "$BASE/api/auth/sign-out" -H "Origin: $BASE" -o /dev/null +else + echo "4) Giriş testi atlandı (SMOKE_USER ve SMOKE_PASSWORD tanımlı değil)" +fi + +echo +if [ "$FAIL" = 0 ]; then echo "Sonuç: tüm kontroller geçti."; else echo "Sonuç: başarısız kontrol var."; fi +exit "$FAIL"