Files
sicabe-core/deploy/lib/common.sh
T
2026-10-12 14:26:44 +07:00

276 lines
14 KiB
Bash
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
# =============================================================================
# eBdesk SiCABE — Common Library (shared by install.sh, bootstrap.sh, test-verification.sh)
# =============================================================================
# ─── Color & Styling ─────────────────────────────────────────────────────────
export RED='\033[0;31m'
export GREEN='\033[0;32m'
export YELLOW='\033[1;33m'
export BLUE='\033[0;34m'
export CYAN='\033[0;36m'
export BOLD='\033[1m'
export NC='\033[0m'
export INFO="${CYAN}[INFO]${NC}"
export OK="${GREEN}[OK]${NC}"
export WARN="${YELLOW}[WARN]${NC}"
export ERR="${RED}[ERROR]${NC}"
export STEP="${BOLD}[STEP]${NC}"
export PASS="${GREEN}[PASS]${NC}"
export FAIL="${RED}[FAIL]${NC}"
# ─── Named Constants (Timeouts) ─────────────────────────────────────────────-
# Default max wait times in seconds (override via env var)
export WAIT_SHORT="${WAIT_SHORT:-30}" # ~30s — quick services
export WAIT_MEDIUM="${WAIT_MEDIUM:-60}" # ~60s — most services
export WAIT_LONG="${WAIT_LONG:-120}" # ~120s — slow services (Keycloak, StarRocks)
export WAIT_EXTRA="${WAIT_EXTRA:-180}" # ~180s — OpenMetadata
export SLEEP_STANDARD="${SLEEP_STANDARD:-2}" # polling interval
export SLEEP_SLOW="${SLEEP_SLOW:-5}" # slow polling (StarRocks)
# ─── Helper: Wait for HTTP endpoint ─────────────────────────────────────────
# Usage: wait_for_service "name" "http://url/health" [max_attempts]
# Selalu return 0 (non-fatal): readiness wait saja. Kegagalan nyata ditangkap
# test-verification.sh. Return non-zero akan mematikan script di bawah `set -e`.
wait_for_service() {
local name="$1"
local url="$2"
local max_attempts="${3:-${WAIT_MEDIUM}}"
local attempt=0
local sleep_time="${SLEEP_STANDARD}"
echo -e "${INFO} Menunggu ${name} siap (maks ${max_attempts}s)..."
while [[ ${attempt} -lt ${max_attempts} ]]; do
if curl -skf "${url}" >/dev/null 2>&1; then
echo -e "${OK} ${name} siap!"
return 0
fi
attempt=$((attempt + sleep_time))
sleep "${sleep_time}"
done
echo -e "${WARN} ${name} belum siap dalam ${max_attempts}s. Lanjutkan... (cek nanti: curl ${url})"
return 0
}
# ─── Helper: Wait for container readiness via Docker inspect ────────────────
# Usage: wait_for_container "container_name" [max_attempts]
wait_for_container() {
local container="$1"
local max_attempts="${2:-${WAIT_MEDIUM}}"
local attempt=0
local sleep_time="${SLEEP_STANDARD}"
echo -e "${INFO} Menunggu container ${container} running..."
while [[ ${attempt} -lt ${max_attempts} ]]; do
if docker ps --format "{{.Names}}" | grep -q "${container}"; then
echo -e "${OK} Container ${container} running."
return 0
fi
attempt=$((attempt + sleep_time))
sleep "${sleep_time}"
done
echo -e "${WARN} Container ${container} belum running dalam ${max_attempts}s."
return 1
}
# ─── Helper: JSON field extractor ───────────────────────────────────────────
# Usage: extract_json "field_name" < <json_string>
# Requires python3
extract_json() {
local field="$1"
python3 -c "import sys,json; print(json.load(sys.stdin).get('${field}',''))" 2>/dev/null || echo ""
}
# ─── Helper: Check if a command exists ──────────────────────────────────────
check_command() {
local cmd="$1"
local name="${2:-$1}"
if command -v "${cmd}" &>/dev/null; then
echo -e "${OK} ${name}: tersedia ($(command -v "${cmd}"))"
return 0
else
echo -e "${WARN} ${name} tidak ditemukan."
return 1
fi
}
# ─── Helper: Print section header ───────────────────────────────────────────
print_section() {
local title="$1"
echo ""
echo -e "${BOLD}${BLUE}═══════════════════════════════════════════════════════${NC}"
echo -e "${BOLD} ${title}${NC}"
echo -e "${BOLD}${BLUE}═══════════════════════════════════════════════════════${NC}"
echo ""
}
# ─── Helper: Muat berkas env (dukung mode multi-host) ───────────────────────
# Prioritas sumber env: --env-file <berkas> > $SICABE_ENV_FILE > <base>/.env
#
# KENAPA: di multi-host, endpoint (POSTGRES_HOST, TRINO_URL, KEYCLOAK_URL, …) BERBEDA
# per host dan ada di `generated/host-<x>/.env` — BUKAN di `deploy/.env`. Tanpa ini
# skrip memakai nilai single-host (localhost) dan memeriksa/menyentuh endpoint salah.
#
# Dipakai: `sicabe_load_env "${SCRIPT_DIR}" "$@"` — GANTI blok `source .env` inline.
# - `--env-file <berkas>` dipilih (relatif dihitung dari <base>).
# - `$SICABE_ENV_FILE` dihormati agar sub-script yang dipanggil `bootstrap.sh --host`
# otomatis mewarisi env host tanpa menimpa dengan deploy/.env.
# - `source` berjalan di shell PEMANGGIL (fungsi tidak membuat subshell), jadi
# variabel tetap terlihat setelah fungsi kembali.
sicabe_load_env() {
local _sicabe_base="$1"; shift
local _sicabe_override="${SICABE_ENV_FILE:-}" _sicabe_prev="" _sicabe_a
for _sicabe_a in "$@"; do
[[ "${_sicabe_prev}" == "--env-file" ]] && _sicabe_override="${_sicabe_a}"
_sicabe_prev="${_sicabe_a}"
done
local _sicabe_envf="${_sicabe_override:-${_sicabe_base}/.env}"
[[ "${_sicabe_envf}" != /* ]] && _sicabe_envf="${_sicabe_base}/${_sicabe_envf}"
if [[ ! -f "${_sicabe_envf}" ]]; then
if [[ -n "${_sicabe_override}" ]]; then
echo -e "${ERR} env-file tidak ditemukan: ${_sicabe_envf}" >&2
return 1
fi
return 0
fi
set -a
# shellcheck disable=SC1090
source "${_sicabe_envf}"
set +a
# Direktori bundle (config + docker-compose.yml + .env) — dipakai helper host-aware
# untuk tahu container mana milik host ini (lihat sicabe_host_expects_container).
SICABE_BUNDLE_DIR="$(dirname "${_sicabe_envf}")"
[[ -n "${_sicabe_override}" ]] && echo -e "${INFO} env-file: ${_sicabe_envf} (mode host)" >&2
return 0
}
# ─── Helper: tujuan rsync default = CERMIN lokasi repo ini di bawah $HOME ────
# Dipakai `install.sh --sync` dan `scripts/check-trino-keystore.sh` supaya nama
# repo TIDAK dipatok (`~/sicabe-doc/deploy`). Repo distribusi bersih (mis.
# `sicabe-core` — isinya hanya folder `deploy/`) pun bekerja tanpa `--dest`:
# /home/u/sicabe-doc/deploy → ~/sicabe-doc/deploy
# /home/u/sicabe-core/deploy → ~/sicabe-core/deploy
# Bila repo di luar $HOME → ~/<nama-folder-induk>/deploy (fallback).
# Mengembalikan path ber-`~` (diekspansi shell REMOTE saat rsync/SSH).
sicabe_default_dest() { # <dir-absolut-dari-script> (default: $SCRIPT_DIR)
local _sdd_dir="${1:-${SCRIPT_DIR:-}}" _sdd_home="${HOME:-}"
[[ -n "${_sdd_dir}" ]] || { echo ""; return 1; }
if [[ -n "${_sdd_home}" && "${_sdd_dir}" == "${_sdd_home}/"* ]]; then
echo "~/${_sdd_dir#"${_sdd_home}/"}"
else
echo "~/$(basename "$(dirname "${_sdd_dir}")")/deploy"
fi
}
# ─── Helper: URL endpoint efektif (sadar EXTERNAL & multi-host) ─────────────
# Urutan: (1) <url_var> eksplisit NON-localhost (komponen EXTERNAL) → pakai apa adanya;
# (2) multi-host (bundle memuat SICABE_HOST_NAME) → scheme://<host_var>:<port>;
# (3) selain itu → scheme://localhost:<port> (single-host).
# Dipakai `scripts/verify-phase*` agar endpoint menunjuk ALAMAT HOST di multi-host,
# tanpa merusak dukungan komponen existing (single-host tetap localhost; external
# tetap memakai URL yang diberikan operator).
sicabe_local_url() { # <scheme> <host_var> <port> [url_var]
local _slu_s="$1" _slu_hv="$2" _slu_p="$3" _slu_uv="${4:-}" _slu_u="" _slu_h=""
[[ -n "${_slu_uv}" ]] && _slu_u="${!_slu_uv:-}"
if [[ -n "${_slu_u}" && "${_slu_u}" != *localhost* && "${_slu_u}" != *127.0.0.1* ]]; then
echo "${_slu_u}"; return 0
fi
_slu_h="${!_slu_hv:-}"
# Remote ⟺ nilai berupa ALAMAT (mengandung titik: IP pemilik, mis. 100.112.76.124).
# Nama layanan tanpa titik (mis. `airflow-webserver`, `keycloak`, `openmetadata-server`)
# = komponen CO-LOCATED → dari host dijangkau via localhost. Uji LAMA ("≠ localhost")
# keliru menganggap nama layanan sebagai remote → `http://airflow-webserver:12082`
# (nama container + port HOST) yang tak bisa diakses dari host.
if [[ -n "${SICABE_HOST_NAME:-}" && "${_slu_h}" == *.* && "${_slu_h}" != "127.0.0.1" ]]; then
echo "${_slu_s}://${_slu_h}:${_slu_p}"; return 0
fi
echo "${_slu_s}://localhost:${_slu_p}"
}
# ─── Helper: URL Keycloak yang REACHABLE dari host ini ──────────────────────
# Prioritas: KEYCLOAK_INTERNAL_URL (URL penuh skema+host+port, mis.
# `https://sso.example.com` — generator menulisnya bila Keycloak EKSTERNAL ber-URL
# penuh) → KEYCLOAK_URL eksplisit non-localhost (override operator) → host+port
# (perilaku lama: nama service bila co-located, alamat pemilik bila lintas host).
# Dipakai gate/verifikasi & bootstrap agar tak perlu set KEYCLOAK_URL manual.
sicabe_keycloak_url() {
if [[ -n "${KEYCLOAK_INTERNAL_URL:-}" ]]; then printf '%s' "${KEYCLOAK_INTERNAL_URL}"; return 0; fi
sicabe_local_url http KEYCLOAK_HOST "${KEYCLOAK_PORT:-12080}" KEYCLOAK_URL
}
# ─── Helper: URL komponen yang REACHABLE dari HOST ──────────────────────────
# Pakai <url_var> bila menunjuk ALAMAT (mengandung titik: IP/FQDN — komponen EKSTERNAL
# atau lintas-host); nilai itu sudah memuat port yang benar. Selebihnya (nama service
# seperti `nessie:19120`, yang TAK resolve dari host / single-host) → <scheme>://host:port
# TERBIT (port host, mis. NESSIE_PORT). Dipakai gate/bootstrap agar tak salah port saat
# komponen eksternal memakai NESSIE_INTERNAL_PORT (mis. 19120) sementara port host 12120.
sicabe_host_url() { # <url_var> <host_var> <port> [scheme=http]
local _shu_u="${!1:-}" _shu_h="${!2:-}" _shu_p="${3:-}" _shu_s="${4:-http}"
if [[ "${_shu_u}" == *//*.* ]]; then printf '%s' "${_shu_u%/}"; return 0; fi
printf '%s://%s:%s' "${_shu_s}" "${_shu_h:-localhost}" "${_shu_p}"
}
# ─── Helper: HOST endpoint efektif (sadar EXTERNAL & multi-host) ────────────
# Urutan: (1) <fallback_var> bila NON-localhost (override operator / komponen EXTERNAL);
# (2) multi-host (SICABE_HOST_NAME) → <host_var>;
# (3) selain itu → localhost.
# Dipakai untuk variabel yang di skrip SEKALIGUS menjadi penanda external
# (mis. STARROCKS_HOST/AIRFLOW_HOST): penanda tetap memakai nilai operator, sedangkan
# koneksi memakai host efektif ini.
sicabe_local_host() { # <host_var> [fallback_var]
local _slh_hv="$1" _slh_fb="${2:-}" _slh_f=""
if [[ -n "${_slh_fb}" ]]; then
_slh_f="${!_slh_fb:-}"
# Remote = ALAMAT (mengandung titik). Nama layanan tanpa titik = co-located → localhost.
if [[ "${_slh_f}" == *.* && "${_slh_f}" != "127.0.0.1" ]]; then
echo "${_slh_f}"; return 0
fi
fi
local _slh_h="${!_slh_hv:-}"
if [[ -n "${SICABE_HOST_NAME:-}" && "${_slh_h}" == *.* && "${_slh_h}" != "127.0.0.1" ]]; then
echo "${_slh_h}"; return 0
fi
echo "localhost"
}
# ─── Helper: apakah container milik HOST INI? ───────────────────────────────
# Single-host (SICABE_HOST_NAME kosong) → selalu true (semua lokal).
# Multi-host (dijalankan dgn `--env-file generated/host-<x>/.env`) → true hanya bila
# container ada di bundle host ini. Dipakai gate agar container host LAIN di-SKIP
# (bukan FAIL) — sama seperti mode host di test-verification.sh.
# Daftar container di-cache (1× `docker compose config` per proses).
sicabe_host_expects_container() { # <nama-container>
[[ -z "${SICABE_HOST_NAME:-}" ]] && return 0
local _shc_dir="${SICABE_BUNDLE_DIR:-.}"
if [[ -z "${SICABE_BUNDLE_CONTAINERS+x}" ]]; then
SICABE_BUNDLE_CONTAINERS="$(docker compose -f "${_shc_dir}/docker-compose.yml" \
--env-file "${_shc_dir}/.env" --profile all config 2>/dev/null \
| python3 -c "import sys,yaml;d=yaml.safe_load(sys.stdin) or {};print('\n'.join((s.get('container_name') or '') for s in (d.get('services') or {}).values()))" \
2>/dev/null || true)"
fi
printf '%s\n' "${SICABE_BUNDLE_CONTAINERS}" | grep -qx "$1"
}
# ─── Helper: wajibkan container LOKAL (kalau bukan → instruksi + kode 3) ─────
# Dipakai skrip seed/reapply yang `docker exec` container tertentu. Di multi-host,
# bila container itu milik host LAIN, skrip harus dijalankan DI host pemiliknya.
sicabe_require_local_container() { # <container> <nama-komponen> <host-contoh>
sicabe_host_expects_container "$1" && return 0
echo -e "${WARN} '${1}' (${2}) berjalan di host LAIN — skrip ini harus dijalankan DI host itu." >&2
echo -e "${WARN} contoh: ./scripts/${0##*/} --env-file generated/host-${3}/.env (di host ${3})" >&2
return 3
}
# ─── Helper: pastikan sumber bind-mount berupa FILE (bukan direktori) ─────────
# Docker membuat DIRECTORY bila sumber bind-mount sebuah file TIDAK ADA saat `up`
# (bind mount ke path non-existent di host). Akibatnya service gagal, mis.
# Airflow SimpleAuthManager: IsADirectoryError '/opt/airflow/simple_auth_manager_pa...'.
# Bila path sudah berupa direktori (sisa `up` sebelumnya) → buang; lalu pastikan ada file.
sicabe_ensure_file_mount() { # <path> [mode]
local path="$1" mode="${2:-644}"
if [[ -d "${path}" ]]; then rm -rf "${path}"; fi
[[ -e "${path}" ]] || : > "${path}"
chmod "${mode}" "${path}" 2>/dev/null || true
}