#!/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" < # 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 > $SICABE_ENV_FILE > /.env # # KENAPA: di multi-host, endpoint (POSTGRES_HOST, TRINO_URL, KEYCLOAK_URL, …) BERBEDA # per host dan ada di `generated/host-/.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 ` dipilih (relatif dihitung dari ). # - `$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 → ~//deploy (fallback). # Mengembalikan path ber-`~` (diekspansi shell REMOTE saat rsync/SSH). sicabe_default_dest() { # (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) eksplisit NON-localhost (komponen EXTERNAL) → pakai apa adanya; # (2) multi-host (bundle memuat SICABE_HOST_NAME) → scheme://:; # (3) selain itu → scheme://localhost: (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() { # [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: HOST endpoint efektif (sadar EXTERNAL & multi-host) ──────────── # Urutan: (1) bila NON-localhost (override operator / komponen EXTERNAL); # (2) multi-host (SICABE_HOST_NAME) → ; # (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() { # [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-/.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() { # [[ -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() { # 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() { # [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 }