2026-10-09 02:35:14 +07:00
#!/usr/bin/env bash
# =============================================================================
# eBdesk SiCABE — Installer
# =============================================================================
# Sesuai urutan deployment berdasarkan Dependency Graph (bab 3.1 & 12.2):
# Phase 1: Governance (IAM) → Keycloak
# Phase 2: Storage → MinIO → Nessie
# Phase 3: Processing → Trino
# Phase 4: Serving → StarRocks
# Phase 5: Ingestion → Airflow
# Phase 6: Governance (Lan) → OpenMetadata → Grafana
#
# Usage:
# ./install.sh # Deploy semua phase
# ./install.sh --phase 1 # Deploy hanya phase tertentu
# ./install.sh --phase 1-3 # Deploy phase 1 sampai 3
# ./install.sh --status # Cek status semua komponen
# ./install.sh --destroy # Hentikan dan hapus semua container
# ./install.sh --help # Tampilkan bantuan
# =============================================================================
set -euo pipefail
# ─── Base Directory ─────────────────────────────────────────────────────────
SCRIPT_DIR = " $( cd " $( dirname " ${ BASH_SOURCE [0] } " ) " && pwd ) "
cd " ${ SCRIPT_DIR } "
# ─── Shared Library ──────────────────────────────────────────────────────────
source " ${ SCRIPT_DIR } /lib/common.sh"
# ─── Color & Styling (overrides untuk install.sh) ────────────────────────────
STEP = " ${ BOLD } [ ${ NC } "
# ─── Generate .env jika belum ada ──────────────────────────────────────────
# KECUALI mode multi-host (--host): rahasia untuk host itu sudah ada di
# generated/host-<x>/.env. Membuat .env acak di server ini BERBAHAYA karena rahasianya
# akan BEDA dari host lain (client secret OIDC, password DB, service account), dan
# berkas itu akan dipakai diam-diam oleh pemanggilan TANPA --host (bootstrap.sh,
# test-verification.sh, gate fase) sehingga muncul kegagalan login yang sulit dilacak.
HOST_MODE_ARG = 0
ENV_WAS_CREATED = 0
for _arg in " $@ " ; do
[[ " ${ _arg } " == "--host" ]] && HOST_MODE_ARG = 1
done
if [[ ${ HOST_MODE_ARG } -eq 1 && ! -f " ${ SCRIPT_DIR } /.env" ]] ; then
echo -e " ${ INFO } Mode --host: .env lokal TIDAK dibuat (memakai generated/host-<x>/.env)"
fi
if [[ ${ HOST_MODE_ARG } -eq 0 && ! -f " ${ SCRIPT_DIR } /.env" ]] ; then
echo -e " ${ INFO } Membuat .env dengan password acak..."
echo -e " ${ WARN } Password tersimpan di .env — JANGAN commit file ini ke git!"
# PERHATIAN: Password PostgreSQL hanya diproses SAAT PERTAMA container dibuat.
# Jika sudah pernah deploy lalu mengubah password di .env, jalankan:
# docker compose down -v && docker volume rm sicabe_postgres-data
# untuk menghapus volume lama.
cat > " ${ SCRIPT_DIR } /.env" <<EOF
# eBdesk SiCABE — Environment File (auto-generated by install.sh)
# Override nilai di bawah sesuai kebutuhan production.
# PostgreSQL: 1 instance shared untuk semua service (beda database)
POSTGRES_PASSWORD=$(openssl rand -hex 16)
KEYCLOAK_DB_PASSWORD=$(openssl rand -hex 16)
NESSIE_DB_PASSWORD=$(openssl rand -hex 16)
AIRFLOW_DB_PASSWORD=$(openssl rand -hex 16)
OPENMETADATA_DB_PASSWORD=$(openssl rand -hex 16)
KEYCLOAK_ADMIN_PASSWORD=$(openssl rand -hex 8)
MINIO_ROOT_PASSWORD=$(openssl rand -hex 16)
AIRFLOW_ADMIN_PASSWORD=$(openssl rand -hex 8)
AIRFLOW_SECRET_KEY=$(openssl rand -hex 32)
TRINO_SVC_SEED_PASSWORD=$(openssl rand -hex 8)
TRINO_SVC_DBT_PASSWORD=$(openssl rand -hex 8)
TRINO_SVC_AIRFLOW_PASSWORD=$(openssl rand -hex 8)
TRINO_SVC_OM_PASSWORD=$(openssl rand -hex 8)
2026-10-11 13:59:47 +07:00
TRINO_OAUTH_STATE_KEY=$(openssl rand -hex 32)
TRINO_OAUTH_STATE_KEY=$(openssl rand -hex 32)
2026-10-09 02:35:14 +07:00
GRAFANA_ADMIN_PASSWORD=$(openssl rand -hex 8)
2026-10-11 13:59:47 +07:00
STARROCKS_ROOT_PASSWORD=$(openssl rand -hex 16)
2026-10-09 02:35:14 +07:00
# OM 2.0.x menegakkan kebijakan password (min 8 karakter + huruf besar + huruf
# kecil + digit + karakter spesial). `openssl rand -hex` hanya menghasilkan
# 0-9a-f sehingga TIDAK memenuhi kebijakan itu — password harus dirakit agar
# keempat kelas karakter dijamin ada.
OPENMETADATA_ADMIN_PASSWORD="$(openssl rand -hex 12)Aa1!"
LDAP_ADMIN_PASSWORD=$(openssl rand -hex 8)
LDAP_CONFIG_PASSWORD=$(openssl rand -hex 8)
# OIDC Client Secrets (harus sinkron dengan config .properties/.yaml files)
TRINO_CLIENT_SECRET=$(openssl rand -hex 16)
STARROCKS_CLIENT_SECRET=$(openssl rand -hex 16)
AIRFLOW_CLIENT_SECRET=$(openssl rand -hex 16)
OPENMETADATA_CLIENT_SECRET=$(openssl rand -hex 16)
GRAFANA_CLIENT_SECRET=$(openssl rand -hex 16)
EOF
chmod 600 " ${ SCRIPT_DIR } /.env"
echo -e " ${ OK } .env dibuat dengan password acak."
ENV_WAS_CREATED = 1
fi
# ─── Load Default Environment ───────────────────────────────────────────────
source " ${ SCRIPT_DIR } /config/environment.defaults.sh"
# ─── Load .env jika ada ─────────────────────────────────────────────────────
if [[ -f " ${ SCRIPT_DIR } /.env" ]] ; then
set -a
source " ${ SCRIPT_DIR } /.env"
set +a
echo -e " ${ INFO } Loaded overrides from .env"
fi
# ─── Fungsi Bantuan ──────────────────────────────────────────────────────────
usage() {
echo -e " ${ BOLD } Usage: ${ NC } "
echo " $0 # Deploy semua phase (1-6)"
echo " $0 --phase N # Deploy phase tertentu (1-6)"
echo " $0 --phase N-M # Deploy range phase (contoh: 1-3)"
echo " $0 --status [--host <nama>] # Cek status container (--host: pakai bundle host)"
echo " $0 --logs [service] # Lihat logs service (atau semua)"
echo " $0 --destroy # Hentikan dan hapus semua container"
echo " $0 --reset-ldap # Reset volume OpenLDAP (seed ulang user/grup)"
echo " $0 --restart [service] # Restart service tertentu"
echo ""
echo -e " ${ BOLD } Multi-host (Fase 1): ${ NC } "
echo " $0 --generate [--inventory F] [--blueprint a3|b5]"
echo " # Hasilkan generated/host-<x>/ dari cluster.yml"
echo " $0 --host <nama> [--dry-run] # Jalankan compose untuk satu host (di host itu!)"
echo " $0 --preflight [--inventory F]"
echo " # Cek keterjangkauan lintas host (tanpa deploy)"
echo " $0 --sync [--host <nama>] [--dest DIR] [--user U] [--delete] [--dry-run]"
echo " # Salin deploy/ (termasuk .env) ke host lain (dari control)"
echo " $0 --help # Tampilkan bantuan ini"
echo ""
echo -e " ${ BOLD } Phase: ${ NC } "
echo " 1 = Keycloak (Governance: IAM)"
echo " 2 = MinIO + Nessie (Storage)"
echo " 3 = Trino (Processing)"
echo " 4 = StarRocks (Serving)"
echo " 5 = Airflow (Ingestion)"
echo " 6 = OpenMetadata + Grafana (Governance: Metadata & Observability)"
echo ""
echo -e " ${ BOLD } Komponen existing (dikelola sistem lain): ${ NC } "
echo " Arahkan endpoint di .env → container komponen itu TIDAK dinyalakan:"
echo " KEYCLOAK_URL · LDAP_URL · S3_ENDPOINT · NESSIE_URL · TRINO_URL · OPA_URL"
echo " Override eksplisit (mis. Postgres milik DBA):"
echo " EXTERNAL_SERVICES=\"keycloak postgres\" LOCAL_SERVICES=\"minio\""
exit 0
}
# ─── Fungsi Prasyarat ─────────────────────────────────────────────────────────
check_prerequisites() {
echo -e " ${ INFO } Memeriksa prasyarat..."
# Docker
if ! command -v docker & >/dev/null; then
echo -e " ${ ERR } Docker tidak ditemukan. Install Docker terlebih dahulu."
echo " https://docs.docker.com/engine/install/"
exit 1
fi
echo -e " ${ OK } Docker: $( docker --version) "
# Docker Compose
if command -v docker-compose & >/dev/null; then
DOCKER_COMPOSE = "docker-compose"
elif docker compose version & >/dev/null; then
DOCKER_COMPOSE = "docker compose"
else
echo -e " ${ ERR } Docker Compose tidak ditemukan."
exit 1
fi
echo -e " ${ OK } Docker Compose: $( ${ DOCKER_COMPOSE } version 2>/dev/null || docker compose version) "
# mc (MinIO client) — opsional untuk bootstrap
if command -v mc & >/dev/null; then
echo -e " ${ OK } MinIO Client (mc): tersedia"
else
echo -e " ${ WARN } MinIO Client (mc) tidak ditemukan. Beberapa fitur bootstrap mungkin terbatas."
fi
# OpenSSL
if ! command -v openssl & >/dev/null; then
echo -e " ${ ERR } OpenSSL tidak ditemukan. Install openssl."
exit 1
fi
# curl
if ! command -v curl & >/dev/null; then
echo -e " ${ ERR } curl tidak ditemukan."
exit 1
fi
echo -e " ${ OK } curl tersedia"
# htpasswd (httpd-tools) — generate password.db service account Trino
if ! command -v htpasswd & >/dev/null; then
echo -e " ${ ERR } htpasswd tidak ditemukan (dibutuhkan untuk password service account Trino)."
echo " Install: Debian/Ubuntu → sudo apt-get install -y apache2-utils"
echo " RHEL/Fedora → sudo dnf install -y httpd-tools"
echo " macOS → brew install httpd"
exit 1
fi
echo -e " ${ OK } htpasswd: tersedia"
# python3 (untuk JSON parsing di bootstrap.sh & test-verification.sh)
if ! command -v python3 & >/dev/null; then
echo -e " ${ WARN } python3 tidak ditemukan. Beberapa fungsi bootstrap dan test mungkin terbatas."
echo " Install python3 atau pastikan tersedia di PATH."
else
echo -e " ${ OK } python3: tersedia"
fi
# Pastikan direktori config ada
for dir in config/keycloak config/trino/catalog config/starrocks config/grafana/datasources config/grafana/dashboards config/airflow config/nessie config/opa config/openldap; do
if [[ ! -d " ${ SCRIPT_DIR } / ${ dir } " ]] ; then
echo -e " ${ WARN } Direktori ${ dir } tidak ditemukan, membuat..."
mkdir -p " ${ SCRIPT_DIR } / ${ dir } "
fi
done
echo -e " ${ OK } Semua prasyarat terpenuhi."
echo ""
}
# ─── Generate password.db service account Trino (bcrypt) ─────────────────────
# Pra-syarat: htpasswd. Harus dijalankan SEBELUM Trino start (bind-mount file
# harus ada; jika tidak, Docker membuat DIRECTORY di path tsb).
generate_trino_password_db() {
local pw_file = " ${ SCRIPT_DIR } /config/trino/password.db"
# Wajib FILE (bukan direktori): Docker membuat DIRECTORY bila sumber bind-mount
# tidak ada saat `up`. `sicabe_ensure_file_mount` membuang sisa direktori & membuat file.
sicabe_ensure_file_mount " ${ pw_file } " 644
local pair user pass
for pair in \
"svc_seed: ${ TRINO_SVC_SEED_PASSWORD :? } " \
"svc_dbt: ${ TRINO_SVC_DBT_PASSWORD :? } " \
"svc_airflow: ${ TRINO_SVC_AIRFLOW_PASSWORD :? } " \
"svc_om: ${ TRINO_SVC_OM_PASSWORD :? } " ; do
user = " ${ pair %%:* } " ; pass = " ${ pair #*: } "
htpasswd -nbB -C 10 " ${ user } " " ${ pass } " | sed '/^$/d' >> " ${ pw_file } "
done
chmod 644 " ${ pw_file } "
echo -e " ${ OK } password.db di-generate: $( cut -d: -f1 " ${ pw_file } " | paste -sd, -) "
}
# ─── Deteksi komponen EXISTING (dikelola sistem lain) ────────────────────────
# Prinsip: docker-compose.yml hanya untuk komponen yang MEMANG dijalankan di sini.
# Komponen yang endpoint-nya sudah diarahkan ke luar tidak dinyalakan, sehingga
# tidak ada container kembar / bentrok port dengan sistem yang sudah ada.
# Lihat config/environment.defaults.sh untuk tabel variabelnya.
# Nilai "lokal" (sesuai docker-compose.yml) untuk tiap komponen.
local_endpoint_of() {
case " $1 " in
keycloak) echo "http://localhost: ${ KEYCLOAK_PORT :- 12080 } " ;;
openldap) echo "ldap://openldap:389" ;;
minio) echo "http://minio:9000" ;;
nessie) echo "http://nessie:19120" ;;
trino) echo "https://localhost: ${ TRINO_HTTPS_PORT :- 12443 } " ;;
opa) echo "http://localhost: ${ OPA_PORT :- 12181 } " ;;
*) echo "" ;;
esac
}
# Endpoint yang sedang dikonfigurasi (.env / defaults).
configured_endpoint_of() {
case " $1 " in
keycloak) echo " ${ KEYCLOAK_URL :- } " ;;
openldap) echo " ${ LDAP_URL :- } " ;;
minio) echo " ${ S3_ENDPOINT :- } " ;;
nessie) echo " ${ NESSIE_URL :- } " ;;
trino) echo " ${ TRINO_URL :- } " ;;
opa) echo " ${ OPA_URL :- } " ;;
*) echo "" ;;
esac
}
# Daftar service yang dikelola EKSTERNAL (spasi-separated, unik).
# Nama LOGIS -> nama service sebenarnya.
#
# Filter komponen eksternal mencocokkan NAMA SERVICE (`starrocks-fe`, `trino-worker`,
# …). Tanpa pemetaan ini, menulis nama logis di EXTERNAL_SERVICES/LOCAL_SERVICES
# diam-diam TIDAK berefek: `EXTERNAL_SERVICES=starrocks` tidak melewati apa pun, dan
# `LOCAL_SERVICES=starrocks` tidak memaksa apa pun. Dulu hanya `trino` yang istimewa,
# sehingga orang yang menulis `starrocks` merasa setelannya diabaikan.
expand_service_names() { # <nama-logis|nama-service>...
local n out = ""
for n in " $@ " ; do
case " ${ n } " in
trino) out += " trino-coordinator trino-worker " ;;
starrocks) out += " starrocks-fe starrocks-cn " ;;
airflow) out += " airflow-init airflow-webserver airflow-scheduler airflow-dag-processor " ;;
openmetadata) out += " openmetadata-server openmetadata-opensearch " ;;
*) out += " ${ n } " ;;
esac
done
printf '%s' " ${ out # } "
}
external_services() {
local out = " $( expand_service_names ${ EXTERNAL_SERVICES :- } ) " svc cfg local_val
for svc in keycloak openldap minio nessie trino opa; do
cfg = " $( configured_endpoint_of " ${ svc } " ) "
local_val = " $( local_endpoint_of " ${ svc } " ) "
if [[ -n " ${ cfg } " && -n " ${ local_val } " && " ${ cfg } " != " ${ local_val } " ]] ; then
out += " ${ svc } "
fi
done
# S3 eksternal ⇒ minio-init (pembuat bucket lokal) tidak berlaku.
if [[ " ${ out } " == *" minio " * ]] ; then
out += " minio-init "
fi
# Trino kini DUA service (coordinator + worker): nama logis `trino` dipetakan ke
# keduanya agar deteksi eksternal benar-benar melewati container-nya.
if [[ " ${ out } " == *" trino " * ]] ; then
out = " $( echo " ${ out } " | sed 's/ trino / /' ) "
out += " trino-coordinator trino-worker "
fi
# StarRocks: FE & CN dikelola sistem lain bila hostnya bukan localhost.
if [[ -n " ${ STARROCKS_HOST :- } " && " ${ STARROCKS_HOST } " != "localhost" && " ${ STARROCKS_HOST } " != "127.0.0.1" ]] ; then
out += " starrocks-fe starrocks-cn "
fi
# Airflow: seluruh service fase 5 dikelola sistem lain bila hostnya bukan localhost.
if [[ -n " ${ AIRFLOW_HOST :- } " && " ${ AIRFLOW_HOST } " != "localhost" && " ${ AIRFLOW_HOST } " != "127.0.0.1" ]] ; then
out += " airflow-init airflow-webserver airflow-dag-processor airflow-scheduler "
fi
# OpenMetadata / Grafana: dikelola sistem lain bila URL host-nya bukan localhost.
if [[ -n " ${ OPENMETADATA_URL :- } " && " ${ OPENMETADATA_URL } " != "http://localhost: ${ OPENMETADATA_PORT :- 12085 } " ]] ; then
out += " openmetadata-opensearch openmetadata-migrate openmetadata-server "
fi
if [[ -n " ${ GRAFANA_URL :- } " && " ${ GRAFANA_URL } " != "http://localhost: ${ GRAFANA_PORT :- 12300 } " ]] ; then
out += " grafana "
fi
# LOCAL_SERVICES: paksa komponen tertentu tetap dijalankan lokal.
# Nama logis (`trino`, `starrocks`, `airflow`, `openmetadata`) diuraikan oleh
# expand_service_names() supaya cocok dengan nama service yang sebenarnya.
local s
for s in $( expand_service_names ${ LOCAL_SERVICES :- } ) ; do
out = " $( echo " ${ out } " | sed "s/ ${ s } / /g" ) "
done
# PENTING (regresi e099b95): JANGAN pakai `grep -v '^$'` di sini. Saat TIDAK ada
# service eksternal, input hanya baris kosong → grep tidak menemukan baris cocok →
# exit 1 → dengan `set -euo pipefail` assignment `EXT_SVCS="$(external_services)"`
# MEMATIKAN install.sh tepat setelah banner (tanpa pesan error). `awk 'NF'` setara
# (buang baris kosong) tetapi SELALU exit 0.
echo " ${ out } " | tr ' ' '\n' | awk 'NF' | sort -u | paste -sd' ' -
}
is_external() {
[[ " $( external_services) " == *" $1 " * ]]
}
# Service yang harus dinyalakan untuk sebuah fase:
# kandidat dari profil compose DIKURANGI komponen eksternal.
phase_services() {
local phase = " $1 " svc out = ""
local ext = " $( external_services) "
while read -r svc; do
[[ -z " ${ svc } " ]] && continue
if [[ " ${ ext } " == *" ${ svc } " * ]] ; then
continue
fi
out += " ${ svc } "
done < <( ${ DOCKER_COMPOSE } -f " ${ SCRIPT_DIR } /docker-compose.yml" --profile "phase ${ phase } " config --services 2>/dev/null)
echo " ${ out # } "
}
# Naikkan container satu fase.
# - Tanpa komponen eksternal: perilaku lama (`--profile phaseN up -d`) supaya
# depends_on tetap mengatur urutan + menunggu healthcheck.
# - Dengan komponen eksternal: hanya service lokal, `--no-deps` agar depends_on
# tidak menyeret service yang dikelola sistem lain. Postgres (bila lokal)
# dinaikkan lebih dulu supaya dependennya tidak menabrak DB yang belum siap.
deploy_phase_compose() {
local phase = " $1 " svcs
svcs = " $( phase_services " ${ phase } " ) "
# Dir bind-mount yang absen di git (config/airflow/sling hanya env.yaml gitignored):
# bila tidak ada, Docker (root) membuatnya sebagai ROOT → operator non-root gagal
# menulis env.yaml lewat bootstrap. Pastikan ada & user-owned sebelum `up`.
mkdir -p " ${ SCRIPT_DIR } /config/airflow/sling"
if [[ -z " $( external_services) " ]] ; then
${ DOCKER_COMPOSE } -f " ${ SCRIPT_DIR } /docker-compose.yml" --profile "phase ${ phase } " up -d
return 0
fi
if [[ -z " ${ svcs } " ]] ; then
echo -e " ${ INFO } Semua komponen fase ${ phase } dikelola eksternal — tidak ada container yang dinyalakan."
return 0
fi
echo -e " ${ INFO } Mode existing — container fase ${ phase } yang dinyalakan: ${ BOLD }${ svcs }${ NC } "
if [[ " ${ svcs } " == *" postgres " * ]] ; then
${ DOCKER_COMPOSE } -f " ${ SCRIPT_DIR } /docker-compose.yml" --profile all up -d --no-deps postgres
wait_for_container "sicabe-postgres" 60
fi
# shellcheck disable=SC2086
${ DOCKER_COMPOSE } -f " ${ SCRIPT_DIR } /docker-compose.yml" --profile all up -d --no-deps ${ svcs }
}
# Tunggu komponen: bila eksternal, tunggu endpoint eksternalnya (bukan localhost).
wait_component() {
local svc = " $1 " local_url = " $2 " timeout = " $3 " ext_url = " ${ 4 :- } "
if is_external " ${ svc } " ; then
if [[ " ${ ext_url } " == http* ]] ; then
echo -e " ${ INFO } ${ svc } dikelola eksternal — menunggu ${ ext_url } "
wait_for_service " ${ svc } (eksternal)" " ${ ext_url } " " ${ timeout } "
else
echo -e " ${ INFO } ${ svc } dikelola eksternal — lewati penantian (diverifikasi gate fase)"
fi
return 0
fi
wait_for_service " ${ svc } " " ${ local_url } " " ${ timeout } "
}
# ─── Fungsi Deploy per Phase ─────────────────────────────────────────────────
deploy_phase() {
local phase = " $1 "
local compose_file = " ${ SCRIPT_DIR } /docker-compose.yml"
case " ${ phase } " in
1)
echo -e " ${ STEP } Phase 1 ${ NC } — Governance (IAM): Keycloak"
deploy_phase_compose 1
wait_component "keycloak" "http://localhost: ${ KEYCLOAK_PORT :- 12080 } /health/ready" 120 \
" ${ KEYCLOAK_URL :- } /health/ready"
if is_external openldap; then
echo -e " ${ INFO } OpenLDAP dikelola eksternal (LDAP_URL= ${ LDAP_URL } ) — lewati penantian container"
else
wait_for_container "sicabe-openldap" 60
fi
echo -e " ${ OK } Phase 1 selesai — Keycloak ${ KEYCLOAK_URL } , OpenLDAP ${ LDAP_URL } "
;;
2)
echo -e " ${ STEP } Phase 2 ${ NC } — Storage: MinIO + Nessie"
deploy_phase_compose 2
wait_component "minio" "http://localhost: ${ MINIO_PORT :- 12900 } /minio/health/live" 60
wait_component "nessie" "http://localhost: ${ NESSIE_PORT :- 12120 } /api/v2/config" 90 \
"http:// ${ NESSIE_HOST :- localhost } : ${ NESSIE_PORT :- 12120 } /api/v2/config"
echo -e " ${ OK } Phase 2 selesai — object storage ${ S3_ENDPOINT } , Nessie ${ NESSIE_URL } "
;;
3)
echo -e " ${ STEP } Phase 3 ${ NC } — Processing: Trino + OPA"
if is_external trino; then
echo -e " ${ INFO } Trino dikelola eksternal ( ${ TRINO_URL } ) — password.db lokal tidak dibuat/dipakai"
else
generate_trino_password_db
fi
deploy_phase_compose 3
wait_component "trino" "https://localhost: ${ TRINO_HTTPS_PORT :- 12443 } /v1/info" 90 \
" ${ TRINO_URL :- } /v1/info"
echo -e " ${ OK } Phase 3 selesai — Trino ${ TRINO_URL } (OAuth2 + Basic) · OPA ${ OPA_URL } "
;;
4)
echo -e " ${ STEP } Phase 4 ${ NC } — Serving: StarRocks ${ BOLD }${ NC } "
deploy_phase_compose 4
if is_external starrocks-fe; then
echo -e " ${ INFO } StarRocks dikelola eksternal — lewati penantian start"
else
echo -e " ${ INFO } StarRocks FE membutuhkan waktu ~2 menit untuk start..."
sleep 30
fi
echo -e " ${ OK } Phase 4 selesai — StarRocks FE : ${ STARROCKS_FE_PORT :- 12930 } , CN (shared-data)"
;;
5)
echo -e " ${ STEP } Phase 5 ${ NC } — Ingestion: Airflow ${ BOLD }${ NC } "
# SimpleAuthManager membuka file ini dengan mode a+ → wajib ADA & writable (666).
# File ini TIDAK di-track git (lihat .gitignore) → generate dari .env di sini
# agar Docker tidak membuat bind-mount file ini sebagai DIRECTORY (fresh clone).
if is_external airflow-webserver; then
echo -e " ${ INFO } Airflow dikelola eksternal — lewati pembuatan file auth lokal"
else
local pw_file = " ${ SCRIPT_DIR } /config/airflow/simple_auth_manager_passwords.json"
sicabe_ensure_file_mount " ${ pw_file } " 666
printf '{"admin": "%s"}\n' " ${ AIRFLOW_ADMIN_PASSWORD :?AIRFLOW_ADMIN_PASSWORD belum diset di .env } " > " ${ pw_file } "
chmod 666 " ${ pw_file } "
fi
deploy_phase_compose 5
wait_component "airflow-webserver" "http://localhost: ${ AIRFLOW_PORT :- 12082 } /api/v2/monitor/health" 120
echo -e " ${ OK } Phase 5 selesai — Airflow siap di http://localhost: ${ AIRFLOW_PORT :- 12082 } "
;;
6)
echo -e " ${ STEP } Phase 6 ${ NC } — Governance (Lanjutan): OpenMetadata + Grafana ${ BOLD }${ NC } "
deploy_phase_compose 6
wait_component "openmetadata-server" "http://localhost: ${ OPENMETADATA_PORT :- 12085 } /health" 180
wait_component "grafana" "http://localhost: ${ GRAFANA_PORT :- 12300 } /api/health" 60
echo -e " ${ OK } Phase 6 selesai — OpenMetadata : ${ OPENMETADATA_PORT :- 12085 } , Grafana : ${ GRAFANA_PORT :- 12300 } "
;;
*)
echo -e " ${ ERR } Phase tidak valid: ${ phase } . Gunakan 1-6."
exit 1
;;
esac
}
# ─── Status ──────────────────────────────────────────────────────────────────
show_status() {
# `--status [--host <nama>]`: dengan --host memakai bundle host itu
# (compose + .env hasil generate) sehingga endpoint menunjuk ALAMAT HOST,
# bukan localhost (perbaikan bug multi-host).
local st_host = " ${ 1 :- } "
local compose_file = " ${ SCRIPT_DIR } /docker-compose.yml"
if [[ -n " ${ st_host } " ]] ; then
compose_file = " ${ SCRIPT_DIR } /generated/host- ${ st_host } /docker-compose.yml"
local henv = " ${ SCRIPT_DIR } /generated/host- ${ st_host } /.env"
if [[ ! -f " ${ compose_file } " || ! -f " ${ henv } " ]] ; then
echo -e " ${ ERR } Host ' ${ st_host } ' belum digenerate ( ${ compose_file } )."
echo " Jalankan dulu: ./install.sh --generate"
return 1
fi
set -a; source " ${ henv } " ; set +a
fi
echo -e " ${ BOLD }${ BLUE } ═══════════════════════════════════════════════════════ ${ NC } "
echo -e " ${ BOLD } STATUS KOMPONEN eBdesk SiCABE ${ NC } "
[[ -n " ${ st_host } " ]] && echo -e " ${ BOLD } (host: ${ st_host } ) ${ NC } "
echo -e " ${ BOLD }${ BLUE } ═══════════════════════════════════════════════════════ ${ NC } "
echo ""
if ! ${ DOCKER_COMPOSE } -f " ${ compose_file } " ps & >/dev/null; then
echo -e " ${ INFO } Tidak ada container SiCABE yang berjalan."
return
fi
echo -e " ${ BOLD } Container Status: ${ NC } "
${ DOCKER_COMPOSE } -f " ${ compose_file } " ps --all 2>/dev/null || \
docker ps --filter "name=sicabe-" --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}"
echo ""
echo -e " ${ BOLD } Endpoint Access: ${ NC } "
# Alamat dasar: alamat host (mode --host) atau localhost (single-host).
local ea = "localhost"
[[ -n " ${ st_host } " ]] && ea = " ${ SICABE_HOST_ADDRESS :- localhost } "
# `*_PUBLIC_URL` (diisi generator) MENANG bila ada. Untuk layanan tanpa URL publik,
# ambil HOST PEMILIK dari variabel endpoint bundle (mode --host), jatuh ke ea.
local h_kc = " $ea " h_minio = " $ea " h_sr = " $ea " h_afs = " $ea " h_om = " $ea " h_ldap = " $ea " h_opa = " $ea " h_nessie = " $ea "
if [[ -n " ${ st_host } " ]] ; then
h_kc = " ${ KEYCLOAK_HOST :- $ea } "
h_minio = " ${ MINIO_HOST :- $ea } "
h_sr = " ${ STARROCKS_FE_HOST :- $ea } "
h_afs = " ${ AIRFLOW_WEBSERVER_HOST :- $ea } "
h_om = " ${ OPENMETADATA_HOST :- $ea } "
if [[ -n " ${ NESSIE_URL :- } " ]] ; then h_nessie = " ${ NESSIE_URL #*:// } " ; h_nessie = " ${ h_nessie %%[:/]* } " ; fi
if [[ -n " ${ OPA_URL :- } " ]] ; then h_opa = " ${ OPA_URL #*:// } " ; h_opa = " ${ h_opa %%[:/]* } " ; fi
if [[ -n " ${ LDAP_URL :- } " ]] ; then h_ldap = " ${ LDAP_URL #*:// } " ; h_ldap = " ${ h_ldap %%[:/]* } " ; fi
fi
echo -e " Keycloak ${ CYAN }${ KEYCLOAK_PUBLIC_URL :- http :// ${ h_kc } : ${ KEYCLOAK_PORT :- 12080 }}${ NC } "
echo -e " OpenLDAP ${ CYAN } ldap:// ${ h_ldap } : ${ OPENLDAP_PORT :- 12389 }${ NC } "
echo -e " MinIO API ${ CYAN } http:// ${ h_minio } : ${ MINIO_PORT :- 12900 }${ NC } "
echo -e " MinIO Console ${ CYAN }${ MINIO_CONSOLE_PUBLIC_URL :- http :// ${ h_minio } : ${ MINIO_CONSOLE_PORT :- 12901 }}${ NC } "
echo -e " Nessie ${ CYAN } http:// ${ h_nessie } : ${ NESSIE_PORT :- 12120 }${ NC } "
echo -e " Trino ${ CYAN }${ TRINO_PUBLIC_URL :- https :// ${ ea } : ${ TRINO_HTTPS_PORT :- 12443 }}${ NC } (OAuth2 + Basic, HTTPS-only)"
echo -e " OPA ${ CYAN } http:// ${ h_opa } : ${ OPA_PORT :- 12181 }${ NC } "
echo -e " StarRocks FE ${ CYAN }${ h_sr } : ${ STARROCKS_FE_PORT :- 12930 }${ NC } (MySQL protocol)"
echo -e " Dremio ${ CYAN } http:// ${ ea } : ${ DREMIO_UI_PORT :- 12947 }${ NC } "
echo -e " Airflow ${ CYAN }${ AIRFLOW_PUBLIC_URL :- http :// ${ h_afs } : ${ AIRFLOW_PORT :- 12082 }}${ NC } "
echo -e " OpenMetadata ${ CYAN }${ OPENMETADATA_PUBLIC_URL :- http :// ${ h_om } : ${ OPENMETADATA_PORT :- 12085 }}${ NC } "
echo -e " Grafana ${ CYAN }${ GRAFANA_PUBLIC_URL :- http :// ${ ea } : ${ GRAFANA_PORT :- 12300 }}${ NC } "
}
# ─── Logs ─────────────────────────────────────────────────────────────────────
show_logs() {
local compose_file = " ${ SCRIPT_DIR } /docker-compose.yml"
if [[ $# -eq 0 ]] ; then
${ DOCKER_COMPOSE } -f " ${ compose_file } " logs --tail= 50 -f
else
${ DOCKER_COMPOSE } -f " ${ compose_file } " logs --tail= 100 -f " $@ "
fi
}
# ─── Destroy ─────────────────────────────────────────────────────────────────
destroy() {
echo -e " ${ YELLOW }${ BOLD } ⚠️ PERINGATAN: Ini akan menghapus semua container dan volume! ${ NC } "
echo -e " ${ YELLOW } Data di volume akan dihapus jika menggunakan --volumes. ${ NC } "
echo ""
echo -n "Lanjutkan? (y/N): "
read -r confirm
if [[ " ${ confirm } " != "y" && " ${ confirm } " != "Y" ]] ; then
echo -e " ${ INFO } Dibatalkan."
exit 0
fi
local compose_file = " ${ SCRIPT_DIR } /docker-compose.yml"
# destroy() tidak memanggil check_prerequisites → DOCKER_COMPOSE bisa belum
# di-set. Deteksi di sini agar tidak "unbound variable" (set -u).
if [[ -z " ${ DOCKER_COMPOSE :- } " ]] ; then
if docker compose version & >/dev/null; then
DOCKER_COMPOSE = "docker compose"
elif docker-compose version & >/dev/null; then
DOCKER_COMPOSE = "docker-compose"
else
echo -e " ${ ERR } Docker Compose tidak ditemukan."
exit 1
fi
fi
echo -e " ${ INFO } Menghentikan container..."
# Semua service di balik `profiles:` → WAJIB `--profile all`, jika tidak
# `down` tidak menyentuh container apa pun (no-op).
${ DOCKER_COMPOSE } -f " ${ compose_file } " --profile all down --remove-orphans 2>/dev/null || true
echo ""
echo -n "Hapus semua volume data? (y/N): "
read -r confirm_vol
if [[ " ${ confirm_vol } " == "y" || " ${ confirm_vol } " == "Y" ]] ; then
echo -e " ${ INFO } Menghapus volume..."
${ DOCKER_COMPOSE } -f " ${ compose_file } " --profile all down -v 2>/dev/null || true
docker volume prune -f --filter "label=com.docker.compose.project=sicabe" 2>/dev/null || true
fi
echo -e " ${ OK } Destroy selesai."
}
# ─── Reset LDAP ──────────────────────────────────────────────────────────────
# Menghapus volume OpenLDAP agar config/openldap/seed.ldif di-apply ulang oleh
# bootstrap.sh. Diperlukan saat nama user/grup berubah, karena seeding di-skip
# bila OU users sudah ada (idempotent).
reset_ldap() {
local compose_file = " ${ SCRIPT_DIR } /docker-compose.yml"
local project = " ${ COMPOSE_PROJECT_NAME :- sicabe } "
if [[ -z " ${ DOCKER_COMPOSE :- } " ]] ; then
if docker compose version & >/dev/null; then DOCKER_COMPOSE = "docker compose"
elif docker-compose version & >/dev/null; then DOCKER_COMPOSE = "docker-compose"
else echo -e " ${ ERR } Docker Compose tidak ditemukan." ; exit 1; fi
fi
echo -n "Reset OpenLDAP (hapus volume user+group, seed ulang via bootstrap)? (y/N): "
read -r confirm
if [[ " ${ confirm } " != "y" && " ${ confirm } " != "Y" ]] ; then
echo -e " ${ INFO } Dibatalkan."
exit 0
fi
echo -e " ${ INFO } Menghentikan OpenLDAP..."
${ DOCKER_COMPOSE } -f " ${ compose_file } " --profile all stop openldap 2>/dev/null || true
echo -e " ${ INFO } Menghapus volume ${ project } _openldap-data & ${ project } _openldap-config..."
docker volume rm " ${ project } _openldap-data" " ${ project } _openldap-config" 2>/dev/null || true
echo -e " ${ INFO } Menjalankan ulang OpenLDAP..."
${ DOCKER_COMPOSE } -f " ${ compose_file } " --profile all up -d openldap 2>/dev/null || true
echo -e " ${ OK } OpenLDAP di-reset. Jalankan './bootstrap.sh' (Step 3) untuk seed ulang."
}
# ─── Main ────────────────────────────────────────────────────────────────────
main() {
local MODE = "all"
local PHASE_START = 1
local PHASE_END = 6
local EXTRA_ARGS =()
if [[ $# -gt 0 ]] ; then
# ── Perintah multi-host (Fase 1) — ditangani lebih dulu ──────────────────
# Catatan: `DOCKER_COMPOSE` baru di-set oleh check_prerequisites(), jadi perintah
# yang butuh Docker memanggilnya lebih dulu; `--generate` justru TIDAK butuh Docker
# (hanya python3+PyYAML) sehingga tidak dipaksa.
case " ${ 1 :- } " in
--generate)
local inv = " ${ SCRIPT_DIR } /cluster.yml" bp = ""
shift
while [[ $# -gt 0 ]] ; do
case " $1 " in
--inventory) inv = " ${ 2 :- } " ; shift 2 ;;
--blueprint) bp = " ${ 2 :- } " ; shift 2 ;;
*) echo -e " ${ ERR } Argumen tidak dikenal untuk --generate: $1 " ; exit 1 ;;
esac
done
if ! command -v python3 & >/dev/null; then
echo -e " ${ ERR } python3 wajib untuk --generate."
exit 1
fi
if ! python3 -c 'import yaml' & >/dev/null; then
echo -e " ${ ERR } PyYAML wajib untuk --generate (pip install pyyaml)."
exit 1
fi
generate_hosts " ${ inv } " " ${ bp } " && exit 0
exit 1
;;
--preflight)
local pinv = " ${ SCRIPT_DIR } /cluster.yml"
shift
while [[ $# -gt 0 ]] ; do
case " $1 " in
--inventory) pinv = " ${ 2 :- } " ; shift 2 ;;
*) echo -e " ${ ERR } Argumen tidak dikenal untuk --preflight: $1 " ; exit 1 ;;
esac
done
if [[ ! -f " ${ pinv } " ]] ; then
echo -e " ${ ERR } Inventory tidak ditemukan: ${ pinv } "
echo " Mulai dari contoh: cp cluster.example.yml cluster.yml"
exit 1
fi
python3 " ${ SCRIPT_DIR } /lib/cluster/preflight.py" " ${ pinv } " \
--compose " ${ SCRIPT_DIR } /docker-compose.yml" --deploy " ${ SCRIPT_DIR } "
exit $?
;;
--sync)
sync_hosts " $@ "
exit $?
;;
--host)
local hname = " ${ 2 :- } " hdry = 0
if [[ -z " ${ hname } " ]] ; then
echo -e " ${ ERR } Sebutkan nama host (lihat: --generate)."
exit 1
fi
shift 2
[[ " ${ 1 :- } " == "--dry-run" ]] && hdry = 1
check_prerequisites # menyetel DOCKER_COMPOSE + memvalidasi Docker
run_host " ${ hname } " " ${ hdry } " && exit 0
exit 1
;;
esac
case " $1 " in
--help| -h) usage ;;
--status)
check_prerequisites
local st_host = ""
[[ " ${ 2 :- } " == "--host" ]] && st_host = " ${ 3 :- } "
show_status " ${ st_host } " ; exit 0 ;;
--logs) check_prerequisites; shift; show_logs " $@ " ; exit 0 ;;
--destroy) destroy; exit 0 ;;
--reset-ldap) reset_ldap; exit 0 ;;
--restart)
check_prerequisites
local compose_file = " ${ SCRIPT_DIR } /docker-compose.yml"
if [[ $# -lt 2 ]] ; then
echo -e " ${ ERR } Sebutkan service yang akan direstart."
echo " $0 --restart keycloak"
exit 1
fi
${ DOCKER_COMPOSE } -f " ${ compose_file } " restart " ${ @: 2 } "
exit $?
;;
--phase)
if [[ $# -lt 2 ]] ; then
echo -e " ${ ERR } Sebutkan phase (1-6 atau range 1-3)."
exit 1
fi
MODE = "phase"
if [[ " $2 " == *-* ]] ; then
PHASE_START = " ${ 2 %-* } "
PHASE_END = " ${ 2 #*- } "
else
PHASE_START = " $2 "
PHASE_END = " $2 "
fi
shift 2
EXTRA_ARGS =( " $@ " )
;;
*)
echo -e " ${ ERR } Argumen tidak dikenal: $1 "
usage
;;
esac
fi
# ─── Mulai Deployment ──────────────────────────────────────────────────────
echo -e " ${ BOLD }${ BLUE } "
echo "╔═══════════════════════════════════════════════════════╗"
echo "║ eBdesk SiCABE — Deployment Script ║"
echo "║ Arsitektur 5 Lapisan — Urutan Berdasar Dependensi ║"
echo "╚═══════════════════════════════════════════════════════╝"
echo -e " ${ NC } "
# Komponen yang dikelola sistem lain → container-nya TIDAK dinyalakan.
local EXT_SVCS
EXT_SVCS = " $( external_services) "
if [[ -n " ${ EXT_SVCS } " ]] ; then
echo -e " ${ INFO } Komponen dikelola EKSTERNAL: ${ BOLD }${ EXT_SVCS }${ NC } "
echo " Container komponen tsb tidak dinyalakan; endpoint diambil dari .env."
echo " Verifikasi setelah bootstrap: ./scripts/verify-phase1-iam.sh dst."
echo ""
fi
check_prerequisites
if [[ " ${ MODE } " == "all" ]] ; then
echo -e " ${ BOLD } Mode: Full Deployment (Semua Phase 1-6) ${ NC } "
echo ""
# Phase 1: Governance (IAM)
echo -e " ${ STEP } ╔═══ Phase 1/6: Governance (IAM) — Keycloak ═══╗ ${ NC } "
deploy_phase 1
# Phase 2: Storage
echo -e " ${ STEP } ╔═══ Phase 2/6: Storage — MinIO → Nessie ═══╗ ${ NC } "
deploy_phase 2
# Phase 3: Processing
echo -e " ${ STEP } ╔═══ Phase 3/6: Processing — Trino ═══╗ ${ NC } "
deploy_phase 3
# Phase 4: Serving
echo -e " ${ STEP } ╔═══ Phase 4/6: Serving — StarRocks ═══╗ ${ NC } "
deploy_phase 4
# Phase 5: Ingestion
echo -e " ${ STEP } ╔═══ Phase 5/6: Ingestion — Airflow ═══╗ ${ NC } "
deploy_phase 5
# Phase 6: Governance (Lanjutan)
echo -e " ${ STEP } ╔═══ Phase 6/6: Governance (Lan) — OpenMetadata → Grafana ═══╗ ${ NC } "
deploy_phase 6
echo ""
echo -e " ${ BOLD }${ GREEN } ═══════════════════════════════════════════════════════ ${ NC } "
echo -e " ${ BOLD }${ GREEN } ✅ SEMUA PHASE SELESAI ${ NC } "
echo -e " ${ BOLD }${ GREEN } Jalankan ./bootstrap.sh untuk inisialisasi & integrasi ${ NC } "
echo -e " ${ BOLD }${ GREEN } Jalankan ./test-verification.sh untuk verifikasi ${ NC } "
echo -e " ${ BOLD }${ GREEN } ═══════════════════════════════════════════════════════ ${ NC } "
echo ""
elif [[ " ${ MODE } " == "phase" ]] ; then
echo -e " ${ BOLD } Mode: Phase ${ PHASE_START } sampai ${ PHASE_END }${ NC } "
for p in $( seq " ${ PHASE_START } " " ${ PHASE_END } " ) ; do
deploy_phase " ${ p } "
done
echo -e " ${ OK } Phase ${ PHASE_START } - ${ PHASE_END } selesai!"
fi
show_status
}
# ─── Multi-host (Fase 1, R10 §4.5) ───────────────────────────────────────────
# Membangkitkan artefak deploy per host ke `generated/host-<x>/`.
generate_hosts() {
local inv = " $1 " blueprint = " $2 " args =()
if [[ ! -f " ${ inv } " ]] ; then
echo -e " ${ ERR } Inventory tidak ditemukan: ${ inv } "
echo " Mulai dari contoh: cp cluster.example.yml cluster.yml (lalu sesuaikan address)"
return 1
fi
# Rahasia untuk SEMUA host diambil dari .env mesin ini (keputusan S1: satu .env
# bersama). Bila .env baru saja dibuat otomatis, rahasianya BARU dan harus menjadi
# satu-satunya sumber: distribusikan generated/ ke semua host, dan JANGAN membuat
# .env terpisah di tiap host (client secret OIDC akan tidak cocok -> login gagal).
if [[ " ${ ENV_WAS_CREATED :- 0 } " -eq 1 ]] ; then
echo -e " ${ WARN } .env BARU SAJA dibuat di mesin ini (password acak)."
echo " .env ini adalah sumber rahasia untuk SEMUA host."
echo " → distribusikan hasil generated/ (di dalamnya rahasia sudah tertanam) ke tiap host;"
echo " → JANGAN membuat .env terpisah di host lain (OIDC client secret harus identik)."
fi
[[ -n " ${ blueprint } " ]] && args +=( " ${ blueprint } " )
echo -e " ${ INFO } Membangkitkan artefak per host dari ${ inv } ..."
python3 " ${ SCRIPT_DIR } /lib/cluster/generate.py" render \
" ${ inv } " " ${ SCRIPT_DIR } /docker-compose.yml" " ${ SCRIPT_DIR } /.env" \
" ${ SCRIPT_DIR } /generated" " ${ SCRIPT_DIR } /scripts/render-config.sh" " ${ args [@] } "
}
# Menjalankan compose untuk SATU host dari hasil generate.
run_host() {
# Catatan bash: JANGAN menggabung `local name="$1" ... dir="...${name}"` dalam satu
# baris — dengan `set -u`, bash meng-expand seluruh kata sebelum assignment sehingga
# `${name}` masih unbound ("name: unbound variable"). Pisahkan per baris.
local name = " $1 "
local dry = " $2 "
local dir = " ${ SCRIPT_DIR } /generated/host- ${ name } "
if [[ ! -d " ${ dir } " ]] ; then
echo -e " ${ ERR } Host ' ${ name } ' belum digenerate (tidak ada ${ dir } )."
echo " Jalankan dulu: $0 --generate [--inventory cluster.yml]"
return 1
fi
# Guard: berkas runtime bind-mount WAJIB FILE (Docker membuat DIRECTORY bila sumber
# hilang → mis. Airflow IsADirectoryError). Isi ditulis setelah .env host di-source.
sicabe_ensure_file_mount " ${ dir } /config/trino/password.db" 644
echo -e " ${ WARN } Jalankan ini HANYA di host ' ${ name } '."
2026-10-11 13:59:47 +07:00
echo -e " ${ WARN } Stack single-host & multi-host TIDAK untuk berjalan bersamaan di satu host:"
echo -e " ${ WARN } matikan stack lama lebih dulu sebelum menyalakan yang baru."
2026-10-09 02:35:14 +07:00
# ── PRECEDENCE (bug nyata yang dilaporkan dari server) ──────────────────────
# Compose memberi prioritas pada variabel **SHELL** di atas `--env-file`. Skrip ini
# sudah men-source config/environment.defaults.sh, yang mengekspor
# POSTGRES_HOST=postgres (default single-host) dan kawan-kawan. Akibatnya nilai dari
# bundle host KALAH, container mencari hostname `postgres` yang hanya ada di host
# lain -> java.net.UnknownHostException: postgres, walaupun --env-file sudah benar.
# Karena itu .env host di-source lebih dulu (set -a) supaya nilai host yang menang.
set -a
# shellcheck disable=SC1091
. " ${ dir } /.env"
set +a
# Berkas runtime bind-mount WAJIB FILE (Docker membuat DIRECTORY bila sumber hilang →
# mis. Airflow SimpleAuthManager IsADirectoryError). Bundle normal sudah memuatnya;
# jaring pengaman ini menulis ulang isi dari .env host.
local _pwp = " ${ dir } /config/airflow/simple_auth_manager_passwords.json"
sicabe_ensure_file_mount " ${ _pwp } " 666
printf '{"admin": "%s"}\n' " ${ AIRFLOW_ADMIN_PASSWORD :?AIRFLOW_ADMIN_PASSWORD tidak ada di ${ dir } /.env } " > " ${ _pwp } "
chmod 666 " ${ _pwp } "
# ── INGRESS (T-20) ─────────────────────────────────────────────────────────
# Service `traefik` berada di profile `edge`. Bila bundle host ini memuatnya
# (artinya tls.mode != none), profile itu WAJIB ikut: tanpa itu Traefik tidak pernah
# start sehingga semua rute publik tidak ada — walaupun label sudah terpasang di
# tiap layanan dan sertifikat sudah bisa diterbitkan. Dideteksi dari BERKAS bundle,
# bukan dari variabel, supaya tidak bisa meleset antara mode none dan edge.
local profiles = "--profile all"
if grep -qE '^ traefik:' " ${ dir } /docker-compose.yml" 2>/dev/null; then
profiles = "--profile all --profile edge"
fi
if [[ " ${ dry } " == "1" ]] ; then
echo -e " ${ INFO } (dry-run) perintah yang akan dijalankan:"
echo " ${ DOCKER_COMPOSE } -f ${ dir } /docker-compose.yml --env-file ${ dir } /.env ${ profiles } up -d"
return 0
fi
${ DOCKER_COMPOSE } -f " ${ dir } /docker-compose.yml" --env-file " ${ dir } /.env" ${ profiles } up -d
}
# ─── Multi-host: distribusi bundle ke host lain (Fase 1) ─────────────────────
# Menyalin direktori deploy/ (TERMASUK .env + generated/) ke host lain via rsync
# over SSH. Dijalankan di host CONTROL (master installer). Target = semua host di
# cluster.yml KECUALI host ini (dideteksi dari kecocokan alamat lokal) — atau satu
# host saja via --host. Aman diulang (rsync idempoten).
2026-10-11 13:59:47 +07:00
# Tujuan default (--dest) = CERMIN lokasi repo ini di bawah $HOME (lihat
# sicabe_default_dest) — jadi nama repo TIDAK dipatok: `sicabe-core` (repo
# distribusi bersih) → `~/sicabe-core/deploy`. Timpa dengan `--dest`.
2026-10-09 02:35:14 +07:00
sync_hosts() {
2026-10-11 13:59:47 +07:00
local inv = " ${ SCRIPT_DIR } /cluster.yml" dest user = " ${ USER :- root } " \
2026-10-09 02:35:14 +07:00
only_host = "" do_delete = 0 dry = 0
2026-10-11 13:59:47 +07:00
dest = " $( sicabe_default_dest " ${ SCRIPT_DIR } " ) "
2026-10-09 02:35:14 +07:00
shift
while [[ $# -gt 0 ]] ; do
case " $1 " in
--inventory) inv = " ${ 2 :- } " ; shift 2 ;;
--dest) dest = " ${ 2 :- } " ; shift 2 ;;
--user) user = " ${ 2 :- } " ; shift 2 ;;
--host) only_host = " ${ 2 :- } " ; shift 2 ;;
--delete) do_delete = 1; shift ;;
--dry-run) dry = 1; shift ;;
*) echo -e " ${ ERR } Argumen tidak dikenal untuk --sync: $1 " ; exit 1 ;;
esac
done
if [[ ! -f " ${ inv } " ]] ; then
echo -e " ${ ERR } Inventory tidak ditemukan: ${ inv } "
echo " Mulai dari contoh: cp cluster.example.yml cluster.yml"
exit 1
fi
if ! command -v rsync & >/dev/null; then
echo -e " ${ ERR } rsync wajib terpasang (mis. sudo apt-get install -y rsync)."
exit 1
fi
local pairs
pairs = " $( python3 " ${ SCRIPT_DIR } /lib/cluster/inventory.py" hosts " ${ inv } " 2>/dev/null) " || {
echo -e " ${ ERR } Gagal membaca inventory ( ${ inv } )." ; exit 1; }
if [[ -z " ${ pairs } " ]] ; then
echo -e " ${ ERR } Inventory tidak memuat host." ; exit 1
fi
# Tentukan host INI dari kecocokan alamat lokal; bila tak cocok → sinkron ke SEMUA.
local self = "" lip n a
lip = " $( hostname -I 2>/dev/null || true ) "
while read -r n a; do
[[ -z " ${ n } " ]] && continue
[[ " ${ lip } " == *" ${ a } " * ]] && self = " ${ n } "
done <<< " ${ pairs } "
# Kumpulkan target: kecuali host ini; hormati --host.
local targets =()
while read -r n a; do
[[ -z " ${ n } " ]] && continue
[[ -n " ${ self } " && " ${ n } " == " ${ self } " ]] && continue
[[ -n " ${ only_host } " && " ${ n } " != " ${ only_host } " ]] && continue
targets +=( " ${ n } | ${ a } " )
done <<< " ${ pairs } "
echo -e " ${ BOLD }${ BLUE } Distribusi deploy/ ke host lain ${ NC } "
echo " sumber : ${ SCRIPT_DIR } /"
echo " tujuan : ${ user } @<host>: ${ dest } "
echo " host ini : ${ self :- <tak dikenal — sinkron ke semua> } "
[[ ${ do_delete } -eq 1 ]] && echo " mode : --delete (hapus berkas tujuan yang tak ada di sumber)"
echo ""
if [[ ${# targets [@] } -eq 0 ]] ; then
echo -e " ${ WARN } Tidak ada host tujuan (host ini= ${ self :- ? } ; --host= ${ only_host :- — } )."
return 0
fi
local t name addr okc = 0 failc = 0
for t in " ${ targets [@] } " ; do
name = " ${ t %%|* } " ; addr = " ${ t ##*| } "
echo -e " ${ STEP } → ${ name } ( ${ user } @ ${ addr } : ${ dest } ) ${ NC } "
local rargs =( -az --human-readable -e "ssh -o ConnectTimeout=8" --exclude '.git' --exclude 'output' )
[[ ${ do_delete } -eq 1 ]] && rargs +=( --delete)
[[ ${ dry } -eq 1 ]] && rargs +=( --dry-run)
# Buat direktori tujuan bila belum ada (`~` diekspansi oleh shell REMOTE).
rargs +=( --rsync-path "mkdir -p ${ dest } && rsync" )
if rsync " ${ rargs [@] } " " ${ SCRIPT_DIR } /" " ${ user } @ ${ addr } : ${ dest } /" ; then
echo -e " ${ OK } ${ name } : tersinkron"
okc = $(( okc + 1 ))
else
echo -e " ${ ERR } ${ name } : GAGAL — periksa SSH/rsync ke ${ addr } " >& 2
failc = $(( failc + 1 ))
fi
done
echo ""
echo -e " ${ BOLD } Ringkasan ${ NC } "
echo " berhasil: ${ okc } · gagal: ${ failc } "
echo " Langkah berikutnya DI tiap host tujuan:"
echo " ./scripts/prefetch-images.sh --host <nama> && ./install.sh --host <nama>"
[[ ${ failc } -eq 0 ]]
}
# ─── Jalankan ─────────────────────────────────────────────────────────────────
main " $@ "