#!/bin/sh # Battani kurulum betiği — https://battani.dev/install.sh # # Tek komutla kurulum (Docker yoksa kurmayı teklif eder): # curl -fsSL https://battani.dev/install.sh | sudo sh # wget -qO- https://battani.dev/install.sh | sudo sh # # Seçenekler (ortam değişkeni karşılığı parantez içinde): # --version X.Y.Z Kurulacak sürüm; varsayılan en yeni sürüm (BATTANI_VERSION) # --dir YOL Kurulum klasörü; varsayılan /opt/battani (BATTANI_DIR) # --port N Web arayüzü portu; varsayılan 4000 (BATTANI_PORT) # --url URL Dışarıdan erişilen adres, ör. https://k8s.firma.com (BATTANI_URL) # --no-registry Gömülü Docker Registry'yi kurma (BATTANI_REGISTRY=0) # --yes Soru sorma, varsayılanlarla devam et (BATTANI_YES=1) # # Betik şunları yapar: Docker ve Compose'u kontrol eder, kurulum klasörüne # docker-compose.yml ve rastgele üretilmiş parolalarla .env yazar, imajı çeker, # servisleri başlatır ve uygulama sağlıklı olana kadar bekler. Veritabanı olarak # aynı compose içindeki PostgreSQL kullanılır; dışarıya port açmaz. set -eu PORTAL_URL="${BATTANI_PORTAL_URL:-https://battani.dev}" IMAGE="${BATTANI_IMAGE:-ghcr.io/buraksumer/battani}" INSTALL_DIR="${BATTANI_DIR:-/opt/battani}" HTTP_PORT="${BATTANI_PORT:-4000}" REGISTRY_PORT="${BATTANI_REGISTRY_PORT:-5000}" VERSION="${BATTANI_VERSION:-}" PUBLIC_URL="${BATTANI_URL:-}" WITH_REGISTRY="${BATTANI_REGISTRY:-1}" ASSUME_YES="${BATTANI_YES:-0}" # Hava boşluklu kurulum: imaj önceden `docker load` ile yüklendiyse çekme. SKIP_PULL="${BATTANI_SKIP_PULL:-0}" # ---------------------------------------------------------------- yardımcılar if [ -t 1 ]; then C_RED=$(printf '\033[31m'); C_GRN=$(printf '\033[32m'); C_YEL=$(printf '\033[33m'); C_BLD=$(printf '\033[1m'); C_OFF=$(printf '\033[0m') else C_RED=''; C_GRN=''; C_YEL=''; C_BLD=''; C_OFF='' fi say() { printf '%s==>%s %s\n' "$C_GRN" "$C_OFF" "$*"; } warn() { printf '%s!!%s %s\n' "$C_YEL" "$C_OFF" "$*" >&2; } die() { printf '%sHATA:%s %s\n' "$C_RED" "$C_OFF" "$*" >&2; exit 1; } # Betik `curl … | sh` ile çalışınca stdin betiğin kendisidir; soruları terminalden oku. ask_yes() { [ "$ASSUME_YES" = "1" ] && return 0 if [ -r /dev/tty ] && { : /dev/null; then printf '%s [E/h] ' "$1" >/dev/tty read -r answer /dev/null 2>&1; then curl -fsSL "$@" elif command -v wget >/dev/null 2>&1; then wget -qO- "$@" else die "curl ya da wget gerekli." fi } fetch_bearer() { # URL TOKEN if command -v curl >/dev/null 2>&1; then curl -fsSL -H "Authorization: Bearer $2" "$1" else wget -qO- --header="Authorization: Bearer $2" "$1" fi } random_secret() { # uzunluk # Yalnız harf ve rakam: .env ve compose içinde kaçış gerektirmez. LC_ALL=C tr -dc 'A-Za-z0-9' /dev/null | sed -n 's/.*"token":"\([^"]*\)".*/\1/p') [ -n "$token" ] || return 1 fetch_bearer "https://ghcr.io/v2/${repo}/tags/list?n=10000" "$token" 2>/dev/null \ | tr ',[]{}' '\n\n\n\n\n' | tr -d ' "' | grep -E '^[0-9]+\.[0-9]+\.[0-9]+$' \ | sort -t. -k1,1n -k2,2n -k3,3n | tail -n 1 } compose() { docker compose --project-directory "$INSTALL_DIR" "$@"; } # Sunucunun kalıcı kimliği: /etc/machine-id; yoksa donanım UUID'si ya da bir # kez üretilen rastgele değer kurulum klasörüne yazılır. host_identity_file() { if [ -s /etc/machine-id ]; then echo /etc/machine-id return fi if [ ! -s "$INSTALL_DIR/host-id" ]; then { cat /sys/class/dmi/id/product_uuid 2>/dev/null || cat /var/lib/dbus/machine-id 2>/dev/null || cat /proc/sys/kernel/random/uuid; } \ | head -n 1 | tr -d ' \n' >"$INSTALL_DIR/host-id" chmod 444 "$INSTALL_DIR/host-id" fi echo "$INSTALL_DIR/host-id" } wait_healthy() { # container, saniye — sağlıklıysa 0; durduysa, çöküp duruyorsa ya da süre dolduysa 1 i=0 while [ "$i" -lt "$2" ]; do info=$(docker inspect -f '{{.State.Status}} {{.RestartCount}} {{if .State.Health}}{{.State.Health.Status}}{{else}}none{{end}}' "$1" 2>/dev/null || echo 'missing 0 none') state=${info%% *}; rest=${info#* }; restarts=${rest%% *}; health=${rest#* } [ "$health" = "healthy" ] && return 0 case "$state" in exited|dead|missing) return 1 ;; esac [ "$restarts" -ge 3 ] && return 1 sleep 5 i=$((i + 5)) done return 1 } # ------------------------------------------------------------------ argümanlar while [ $# -gt 0 ]; do case "$1" in --version) VERSION="${2:?--version bir değer ister}"; shift 2 ;; --dir) INSTALL_DIR="${2:?--dir bir değer ister}"; shift 2 ;; --port) HTTP_PORT="${2:?--port bir değer ister}"; shift 2 ;; --url) PUBLIC_URL="${2:?--url bir değer ister}"; shift 2 ;; --no-registry) WITH_REGISTRY=0; shift ;; -y|--yes) ASSUME_YES=1; shift ;; -h|--help) sed -n '2,20p' "$0" 2>/dev/null || true; exit 0 ;; *) die "Bilinmeyen seçenek: $1 (yardım için --help)" ;; esac done VERSION="${VERSION#v}" printf '\n%sBattani kurulumu%s\n\n' "$C_BLD" "$C_OFF" # ---------------------------------------------------------------- ön kontroller [ "$(uname -s)" = "Linux" ] || die "Bu betik Linux içindir." case "$(uname -m)" in x86_64|amd64) ;; *) die "Battani imajı yalnız x86_64 (amd64) için yayınlanıyor; bu makine: $(uname -m)." ;; esac case "$HTTP_PORT" in ''|*[!0-9]*) die "Geçersiz port: $HTTP_PORT" ;; esac if ! command -v docker >/dev/null 2>&1; then [ "$(id -u)" -eq 0 ] || die "Docker kurulu değil. Docker'ı kurabilmem için betiği sudo ile çalıştırın." warn "Docker kurulu değil." ask_yes "Docker'ı resmi betikle (get.docker.com) kurayım mı?" || die "Docker olmadan devam edilemez." fetch https://get.docker.com | sh command -v systemctl >/dev/null 2>&1 && systemctl enable --now docker >/dev/null 2>&1 || true fi docker info >/dev/null 2>&1 || die "Docker'a erişilemiyor. Betiği sudo ile çalıştırın ya da Docker servisini başlatın." docker compose version >/dev/null 2>&1 || die "Docker Compose eklentisi yok. Kurulum: https://docs.docker.com/compose/install/linux/" if [ -f "$INSTALL_DIR/.env" ]; then die "$INSTALL_DIR içinde zaten bir kurulum var. Güncellemek için: sudo sh $INSTALL_DIR/update.sh" fi if [ -z "$VERSION" ]; then say "En yeni sürüm aranıyor…" VERSION=$(latest_version || true) [ -n "$VERSION" ] || die "En yeni sürüm bulunamadı; --version ile belirtin." fi say "Kurulacak sürüm: ${C_BLD}${VERSION}${C_OFF}" if [ -z "$PUBLIC_URL" ]; then host_ip=$(hostname -I 2>/dev/null | awk '{print $1}') PUBLIC_URL="http://${host_ip:-localhost}:${HTTP_PORT}" fi PUBLIC_URL="${PUBLIC_URL%/}" public_host=$(printf '%s' "$PUBLIC_URL" | sed -e 's#^[a-z]*://##' -e 's#[:/].*$##') # --------------------------------------------------------------------- dosyalar say "Dosyalar yazılıyor: $INSTALL_DIR" umask 077 mkdir -p "$INSTALL_DIR/backups" # Lisans kurulum kimliği bu sunucunun kimliğine bağlanır (salt-okunur bağlanır). host_id_file=$(host_identity_file) admin_password="$(random_secret 20)" profiles='' [ "$WITH_REGISTRY" = "1" ] && profiles='registry' cat >"$INSTALL_DIR/.env" <"$INSTALL_DIR/docker-compose.yml" <<'EOF' # Battani — install.sh tarafından üretildi. Değerler .env dosyasından gelir. # Yönetim: docker compose ps | docker compose logs -f app | docker compose restart app # Güncelleme: sudo sh update.sh name: battani services: postgres: image: postgres:16-alpine container_name: battani-postgres environment: POSTGRES_DB: ${DB_NAME} POSTGRES_USER: ${DB_USER} POSTGRES_PASSWORD: ${DB_PASSWORD} volumes: - postgres-data:/var/lib/postgresql/data healthcheck: test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"] interval: 10s timeout: 3s retries: 10 start_period: 10s restart: unless-stopped networks: - battani app: image: ${BATTANI_IMAGE}:${BATTANI_IMAGE_TAG} container_name: battani-app environment: NODE_ENV: production PORT: "4000" FRONTEND_URL: ${FRONTEND_URL} DB_PROVIDER: postgres DB_SERVER: postgres DB_PORT: "5432" DB_NAME: ${DB_NAME} DB_USER: ${DB_USER} DB_PASSWORD: ${DB_PASSWORD} DB_SSL: "false" JWT_SECRET: ${JWT_SECRET} INITIAL_ADMIN_USERNAME: ${INITIAL_ADMIN_USERNAME} INITIAL_ADMIN_EMAIL: ${INITIAL_ADMIN_EMAIL} INITIAL_ADMIN_PASSWORD: ${INITIAL_ADMIN_PASSWORD} REGISTRY_SECRETS_KEY: ${REGISTRY_SECRETS_KEY} INTEGRATION_SECRETS_KEY: ${INTEGRATION_SECRETS_KEY} REGISTRY_TOKEN_BUNDLE_PATH: /app/config/registry-token-bundle.pem REGISTRY_PUBLIC_HOST: ${REGISTRY_PUBLIC_HOST} REGISTRY_PUBLIC_REALM_URL: ${REGISTRY_TOKEN_REALM} BATTANI_UPDATE_IMAGE: ${BATTANI_IMAGE} BATTANI_IMAGE_TAG: ${BATTANI_IMAGE_TAG} ports: - "${BATTANI_HTTP_PORT}:4000" volumes: - app-config:/app/config # Host identity for the license installation ID (read-only). - ${BATTANI_HOST_ID_FILE}:/etc/host-machine-id:ro depends_on: postgres: condition: service_healthy security_opt: - no-new-privileges:true cap_drop: - ALL restart: unless-stopped networks: - battani registry: image: registry:3 container_name: battani-registry profiles: ["registry"] environment: REGISTRY_AUTH: token REGISTRY_AUTH_TOKEN_REALM: ${REGISTRY_TOKEN_REALM} REGISTRY_AUTH_TOKEN_SERVICE: battani-registry REGISTRY_AUTH_TOKEN_ISSUER: battani-registry REGISTRY_AUTH_TOKEN_ROOTCERTBUNDLE: /auth/registry-token-bundle.pem REGISTRY_STORAGE_DELETE_ENABLED: "true" ports: - "${BATTANI_REGISTRY_PORT}:5000" volumes: - registry-data:/var/lib/registry - app-config:/auth:ro depends_on: app: condition: service_healthy restart: unless-stopped networks: - battani networks: battani: volumes: postgres-data: registry-data: app-config: EOF chmod 644 "$INSTALL_DIR/docker-compose.yml" # Portal bilinmeyen adreslere HTML döner; indirilenin gerçekten betik olduğunu doğrula. if fetch "$PORTAL_URL/update.sh" >"$INSTALL_DIR/update.sh.tmp" 2>/dev/null \ && [ "$(head -c 9 "$INSTALL_DIR/update.sh.tmp")" = '#!/bin/sh' ]; then mv "$INSTALL_DIR/update.sh.tmp" "$INSTALL_DIR/update.sh" chmod 755 "$INSTALL_DIR/update.sh" else rm -f "$INSTALL_DIR/update.sh.tmp" warn "update.sh indirilemedi; güncellemek için: curl -fsSL $PORTAL_URL/update.sh | sudo sh" fi # ---------------------------------------------------------------------- başlat if [ "$SKIP_PULL" != "1" ]; then say "İmajlar indiriliyor (ilk seferde birkaç dakika sürebilir)…" compose pull --quiet fi say "Servisler başlatılıyor…" compose up -d || warn "docker compose up hata verdi; uygulamanın durumu kontrol ediliyor…" say "Uygulamanın açılması bekleniyor (veritabanı hazırlanıyor)…" if ! wait_healthy battani-app 300; then warn "Uygulama 5 dakika içinde sağlıklı hale gelmedi. Son loglar:" docker logs --tail 40 battani-app 2>&1 || true die "Kurulum tamamlanamadı. Loglar: cd $INSTALL_DIR && docker compose logs -f app" fi printf '\n%sBattani %s kuruldu.%s\n\n' "$C_GRN$C_BLD" "$VERSION" "$C_OFF" printf ' Adres : %s\n' "$PUBLIC_URL" printf ' Kullanıcı : admin\n' printf ' Parola : %s (ilk girişten sonra değiştirin)\n' "$admin_password" printf ' Lisans : Açılışta şirket adınızı girin; oluşan Installation ID ile %s üzerinden lisans alın\n' "$PORTAL_URL" printf ' Klasör : %s (.env dosyasını yedekleyin)\n' "$INSTALL_DIR" printf ' Loglar : cd %s && docker compose logs -f app\n' "$INSTALL_DIR" printf ' Güncelleme : sudo sh %s/update.sh\n\n' "$INSTALL_DIR"