2026-10-09 02:35:14 +07:00
#!/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
}
2026-10-11 13:59:47 +07:00
# ─── 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
}
2026-10-09 02:35:14 +07:00
# ─── 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 } "
}
2026-10-11 18:13:03 +07:00
# ─── 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
}
2026-10-09 02:35:14 +07:00
# ─── 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
}