Files
sicabe-core/deploy/lib/common.sh
T

276 lines
14 KiB
Bash
Raw Normal View History

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-12 14:26:44 +07:00
# ─── 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}"
}
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
}