diff --git a/.gitea/workflows/deploy-monitoring-prod.yml b/.gitea/workflows/deploy-monitoring-prod.yml index 54bf271..56667cd 100644 --- a/.gitea/workflows/deploy-monitoring-prod.yml +++ b/.gitea/workflows/deploy-monitoring-prod.yml @@ -68,13 +68,15 @@ jobs: - name: Wait for Loki run: | + source ./common-functions-base.sh + export SPRING_PROFILES_ACTIVE=PROD for i in $(seq 1 36); do REPLICAS=$(docker service ls --filter name=iklimco-monitoring_loki --format "{{.Replicas}}" | head -1) if echo "$REPLICAS" | awk -F'[/ ]' '$1>0 && $1==$2{found=1} END{exit !found}'; then - echo "Loki is ready: $REPLICAS" + log_message "SUCCESS" "Loki is ready: $REPLICAS" exit 0 fi - echo "Loki not ready yet (${REPLICAS:-missing}), waiting 5s..." + log_message "INFO" "Loki not ready yet (${REPLICAS:-missing}), waiting 5s..." sleep 5 done docker service ps iklimco-monitoring_loki || true @@ -82,6 +84,8 @@ jobs: - name: Configure SWAG Reverse Proxy run: | + source ./common-functions-base.sh + export SPRING_PROFILES_ACTIVE=PROD set -a; . ./.env; . ./.env.secrets.swag; set +a export PORTAINER_SUBDOMAIN="${PORTAINER_SUBDOMAIN:-portainer.iklim.co}" export RESTRICTED_IPS_BLOCK="$(echo "$RESTRICTED_IPS" | tr ',' '\n' | sed 's|.*| allow &;|')" @@ -92,16 +96,19 @@ jobs: envsubst "$SWAG_VARS" < "$tpl" | docker run --rm -i \ -v "${SWAG_SITE_CONFS_DIR}:/output" \ alpine sh -c "cat > /output/${fname}" - echo "${fname} written" + log_message "SUCCESS" "${fname} written" done SWAG_CTR=$(docker ps -q -f name=iklimco_swag 2>/dev/null | head -1) if [ -n "$SWAG_CTR" ]; then docker exec "$SWAG_CTR" nginx -t && docker exec "$SWAG_CTR" nginx -s reload + log_message "SUCCESS" "SWAG nginx reloaded" fi - name: Update DNS Records run: | + source ./common-functions-base.sh + export SPRING_PROFILES_ACTIVE=PROD set -a; . ./.env; . ./.env.secrets.swag; set +a FLOATING_IP="${{ vars.PROD_FLOATING_IP }}" DOMAIN="iklim.co" @@ -113,14 +120,14 @@ jobs: 2>/dev/null | jq -r '.[0].data // empty' 2>/dev/null || true) if [ "$CURRENT" = "$FLOATING_IP" ]; then - echo "${record}.${DOMAIN} -> ${FLOATING_IP} exists, skipping" + log_message "INFO" "${record}.${DOMAIN} -> ${FLOATING_IP} exists, skipping" else curl -sf -X PUT \ -H "Authorization: sso-key ${GODADDY_KEY}:${GODADDY_SECRET}" \ -H "Content-Type: application/json" \ "https://api.godaddy.com/v1/domains/${DOMAIN}/records/A/${record}" \ -d "[{\"data\":\"${FLOATING_IP}\",\"ttl\":600}]" - echo "${record}.${DOMAIN} -> ${FLOATING_IP} added/updated" + log_message "SUCCESS" "${record}.${DOMAIN} -> ${FLOATING_IP} added/updated" fi done diff --git a/.gitea/workflows/deploy-monitoring-test.yml b/.gitea/workflows/deploy-monitoring-test.yml index f60e5b3..14b2fab 100644 --- a/.gitea/workflows/deploy-monitoring-test.yml +++ b/.gitea/workflows/deploy-monitoring-test.yml @@ -61,13 +61,15 @@ jobs: - name: Wait for Loki run: | + source ./common-functions-base.sh + export SPRING_PROFILES_ACTIVE=TEST for i in $(seq 1 36); do REPLICAS=$(docker service ls --filter name=iklimco-monitoring_loki --format "{{.Replicas}}" | head -1) if echo "$REPLICAS" | awk -F'[/ ]' '$1>0 && $1==$2{found=1} END{exit !found}'; then - echo "Loki is ready: $REPLICAS" + log_message "SUCCESS" "Loki is ready: $REPLICAS" exit 0 fi - echo "Loki not ready yet (${REPLICAS:-missing}), waiting 5s..." + log_message "INFO" "Loki not ready yet (${REPLICAS:-missing}), waiting 5s..." sleep 5 done docker service ps iklimco-monitoring_loki || true @@ -75,6 +77,8 @@ jobs: - name: Configure SWAG Reverse Proxy run: | + source ./common-functions-base.sh + export SPRING_PROFILES_ACTIVE=TEST set -a; . ./.env; . ./.env.secrets.swag; set +a export PORTAINER_SUBDOMAIN="${PORTAINER_SUBDOMAIN:-portainer-test.iklim.co}" export RESTRICTED_IPS_BLOCK="$(echo "$RESTRICTED_IPS" | tr ',' '\n' | sed 's|.*| allow &;|')" @@ -85,16 +89,19 @@ jobs: envsubst "$SWAG_VARS" < "$tpl" | docker run --rm -i \ -v "${SWAG_SITE_CONFS_DIR}:/output" \ alpine sh -c "cat > /output/${fname}" - echo "${fname} written" + log_message "SUCCESS" "${fname} written" done SWAG_CTR=$(docker ps -q -f name=iklimco_swag 2>/dev/null | head -1) if [ -n "$SWAG_CTR" ]; then docker exec "$SWAG_CTR" nginx -t && docker exec "$SWAG_CTR" nginx -s reload + log_message "SUCCESS" "SWAG nginx reloaded" fi - name: Update DNS Records run: | + source ./common-functions-base.sh + export SPRING_PROFILES_ACTIVE=TEST set -a; . ./.env; . ./.env.secrets.swag; set +a FLOATING_IP="${{ vars.TEST_FLOATING_IP }}" DOMAIN="iklim.co" @@ -106,14 +113,14 @@ jobs: 2>/dev/null | jq -r '.[0].data // empty' 2>/dev/null || true) if [ "$CURRENT" = "$FLOATING_IP" ]; then - echo "${record}.${DOMAIN} -> ${FLOATING_IP} exists, skipping" + log_message "INFO" "${record}.${DOMAIN} -> ${FLOATING_IP} exists, skipping" else curl -sf -X PUT \ -H "Authorization: sso-key ${GODADDY_KEY}:${GODADDY_SECRET}" \ -H "Content-Type: application/json" \ "https://api.godaddy.com/v1/domains/${DOMAIN}/records/A/${record}" \ -d "[{\"data\":\"${FLOATING_IP}\",\"ttl\":600}]" - echo "${record}.${DOMAIN} -> ${FLOATING_IP} added/updated" + log_message "SUCCESS" "${record}.${DOMAIN} -> ${FLOATING_IP} added/updated" fi done diff --git a/common-functions-base.sh b/common-functions-base.sh new file mode 100644 index 0000000..b9a7e38 --- /dev/null +++ b/common-functions-base.sh @@ -0,0 +1,240 @@ +#!/bin/bash + +# ============================================================================== +# 🛠️ BASE UTILITY FUNCTIONS (iklim.co) +# ============================================================================== +# Tüm ortamlar (Dev, Test, Prod) tarafından ortak kullanılan çekirdek fonksiyonlar. +# Bu dosya doğrudan çalıştırılmaz, ortam scriptleri tarafından 'source' edilir. + +# Belirtilen env dosyasını sisteme yükler (export eder). +source_env_file() { + local path="$1" + if [ -f "$path" ]; then + set -o allexport + source "$path" + set +o allexport + fi +} + +# Klasördeki tüm .env.secrets.* dosyalarını otomatik bulur ve yükler. +# (.example ve .shared dosyalarını atlar, onları ana akış yönetir). +source_service_secret_files() { + local file + for file in .env.secrets.*; do + [ -f "$file" ] || continue + [[ "$file" == *.example ]] && continue + [ "$file" = ".env.secrets.shared" ] && continue + source_env_file "$file" + done +} + +# Kritik bir env dosyasının varlığını kontrol eder, yoksa scripti durdurur. +require_env_file() { + local path="$1" + local description="$2" + if [ ! -f "$path" ]; then + log_message "ERROR" "$description not found at $path" + exit 1 + fi +} + +# Env dosyalarındaki değişken isimlerini tarayıp 'envsubst' için liste oluşturur. +# Template dosyalarını doldururken hangi değişkenlerin çözüleceğini belirler. +envsubst_vars_from_files() { + local file + for file in "$ENV_PATH" "$ENV_SECRETS_SHARED_PATH" .env.secrets.*; do + [ -f "$file" ] || continue + [[ "$file" == *.example ]] && continue + grep -E '^[A-Za-z_][A-Za-z_0-9]*=' "$file" | cut -d= -f1 + done | sort -u | sed 's/^/\$/' | tr '\n' ' ' +} + +# Belirtilen bir değişkenin değerini hiyerarşik olarak (env -> shared -> secrets) arar. +lookup_env_value() { + local name="$1" + local file + local value="" + + for file in "$ENV_PATH" "$ENV_SECRETS_SHARED_PATH" .env.secrets.*; do + [ -f "$file" ] || continue + [[ "$file" == *.example ]] && continue + if grep -q "^${name}=" "$file"; then + value="$(grep "^${name}=" "$file" | tail -n1 | cut -d '=' -f2-)" + fi + done + + printf '%s' "$value" +} + +# Matematiksel veya mantıksal işlem gerektiren env değerlerini hesaplar. +# Örn: Milisaniye cinsinden JWT süresini saniyeye çevirir. +refresh_calculated_env_vars() { + export JWT_ACCESS_TOKEN_EXPIRATION_SEC=$(( JWT_ACCESS_TOKEN_EXPIRATION / 1000 )) +} + +# Tüm çevre dosyalarını (ana env, ortak sırlar ve servis sırları) tazeleyerek yükler. +refresh_env_vars() { + source_env_file "$ENV_PATH" + source_env_file "$ENV_SECRETS_SHARED_PATH" + source_service_secret_files + refresh_calculated_env_vars + log_message "INFO" "Environment variables refreshed from all .env and .env.secrets.* files 🔄" +} + +# Bir değişkeni hem shell'e export eder hem de (Dev ortamında) terminale bilgi basar. +export_variable() { + local name="$1" + local value + + if [ $# -ge 2 ]; then + value="$2" + else + log_message "DEBUG" "Looking for ${name} value in env files" + value="$(lookup_env_value "$name")" + fi + + export "${name}=${value}" + if [[ "$ENVIRONMENT" == "dev" ]]; then + log_message "DEBUG" "Env variable ${name} is set to: ${value:0:5}... 🌎" + fi +} + +# --- 🔐 Ortak Vault Yardımcıları --- + +# Vault kilidini açar (Unseal). Dev, Test ve tek-node kurulumlar için VIP path kullanır. +# Prod HA Raft cluster için common-functions-prod.sh bu fonksiyonu override eder. +unseal_vault() { + local vault_addr=$1 + local _curl="curl -s" + [[ "${VAULT_SKIP_VERIFY:-false}" == "true" ]] && _curl="curl -sk" + + local RESPONSE_JSON ERROR_MSG sealed + RESPONSE_JSON=$($_curl $vault_addr/v1/sys/health) + ERROR_MSG=$(echo "$RESPONSE_JSON" | jq -r '.errors[]?') + if [[ -n "$ERROR_MSG" ]]; then + log_message "ERROR" "$ERROR_MSG" + exit 1 + fi + sealed=$(echo $RESPONSE_JSON | jq .sealed) + if [ "$sealed" = "true" ]; then + log_message "INFO" "🔓🗝️ Unsealing Vault ($vault_addr)..." + RESPONSE_JSON=$($_curl --request PUT -H "Content-Type: application/json" \ + --data "{\"key\": \"$VAULT_UNSEAL_KEY\"}" $vault_addr/v1/sys/unseal) + ERROR_MSG=$(echo "$RESPONSE_JSON" | jq -r '.errors[]?') + if [[ -n "$ERROR_MSG" ]]; then + log_message "ERROR" "$ERROR_MSG" + exit 1 + fi + log_message "SUCCESS" "Vault unsealed successfully" + else + log_message "INFO" "Vault is already unsealed" + fi +} + +# --- 📝 Ortak Log Yardımcıları --- + +# Log seviyesini numerik değere çevirir +_get_log_level_num() { + case "${1^^}" in + TRACE) echo 1 ;; + DEBUG) echo 2 ;; + INFO) echo 3 ;; + SUCCESS) echo 4 ;; + WARN) echo 5 ;; + ERROR) echo 6 ;; + FATAL) echo 7 ;; + NONE) echo 99 ;; + *) echo 3 ;; # Default to INFO + esac +} + +# Log seviyesine uygun emojiyi döndürür +_get_log_level_emoji() { + case "${1^^}" in + TRACE) echo "🔎" ;; + DEBUG) echo "🪲" ;; + INFO) echo "ℹ️" ;; + SUCCESS) echo "✅" ;; + WARN) echo "⚠️" ;; + ERROR) echo "❌" ;; + FATAL) echo "☠️" ;; + *) echo "🤔" ;; + esac +} + +# Timestamp'li log fonksiyonu. GLOBAL_LOG_LEVEL'a göre filtreler. +# Kullanım: log_message "INFO" "Bu bir log mesajıdır" +log_message() { + local level="${1^^}" + local message="$2" + + local current_level_num=$(_get_log_level_num "$level") + local global_level_str="${GLOBAL_LOG_LEVEL:-INFO}" + local global_level_num=$(_get_log_level_num "$global_level_str") + local emoji=$(_get_log_level_emoji "$level") + local env_name="${SPRING_PROFILES_ACTIVE:-UNKNOWN}" + env_name="${env_name^^}" + + if [ "$current_level_num" -ge "$global_level_num" ]; then + #local timestamp=$(TZ="Europe/Istanbul" date +"%Y-%m-%dT%H:%M:%S%:z") + local timestamp=$(TZ="Europe/Istanbul" date +"%H:%M:%S") + if [ "$current_level_num" -ge 5 ]; then + # ERROR ve FATAL hata akışına (stderr) basılır + echo "[$timestamp] [$env_name] $emoji [$level] $message" >&2 + else + echo "[$timestamp] [$env_name] $emoji [$level] $message" + fi + fi +} + +# Kayıtlı tüm log dosyalarının son satırlarını basar ve dosyaları temizler. +log_tail() { + for logfile in "${LOG_FILES[@]}"; do + tail -n 5 "$logfile" + rm -f "$logfile" + done +} + +# Bir log dosyası oluşana kadar bekler ve başından (veya bir pattern'den) kesit basar. +log_head() { + local logfile=$1 + local pattern=$2 + local lines=5 + while [ ! -f "$logfile" ] || [ "$(wc -l < "$logfile")" -lt $lines ]; do + sleep 0.5 + done + if [ -z "$pattern" ]; then + head -n $lines "$logfile" + else + local start_line + start_line=$(grep -nm1 "$pattern" "$logfile" | cut -d: -f1) + if [ -z "$start_line" ]; then + head -n $lines "$logfile" + else + sed -n "${start_line},$((start_line + lines - 1))p" "$logfile" + fi + fi +} + +# --- 🐳 Ortak Swarm Yardımcıları --- + +# Docker Swarm üzerindeki bir servisi rolling-restart (güncelleme) yöntemiyle tazeler. +swarm_service_update() { + local stack="$1" + local service="$2" + local image="$3" + local full_name="${stack}_${service}" + if docker service inspect "$full_name" >/dev/null 2>&1; then + log_message "INFO" "🔄 Updating $full_name → $image" + docker service update \ + --image "$image" \ + --with-registry-auth \ + --update-order start-first \ + --update-failure-action rollback \ + "$full_name" + log_message "SUCCESS" "$full_name updated" + else + log_message "ERROR" "Service $full_name does not exist. Manual stack deploy required." + return 1 + fi +}