Files

196 lines
8.1 KiB
Bash
Raw Permalink Normal View History

2026-10-09 02:35:14 +07:00
#!/usr/bin/env bash
# ─────────────────────────────────────────────────────────────────────────────
# render-config.sh — render config/** untuk deploy multi-host (Fase 0/1, R10 §4.9)
#
# MASALAH: file config/** yang dibaca CONTAINER (.properties/.yaml/.cfg/.json/rego)
# TIDAK dapat melakukan ekspansi env saat runtime, berbeda dari docker-compose.yml.
# Pada deploy multi-host, hostname nama container (mis. `nessie:19120`) harus
# menjadi alamat host sebenarnya (mis. `10.20.0.10:19120`).
#
# SOLUSI (Opsi (i) R10 §4.9): `config/` dibiarkan LITERAL (single-host tidak
# tersentuh); script ini menyalinnya ke direktori output sambil mengganti
# hostname TERDAFTAR, menghasilkan `generated/host-<x>/config/` (gitignored).
#
# PRINSIP KEAMANAN
# 1. Hanya hostname yang di-`--map` yang diganti → tidak ada kejutan.
# 2. Penggantian ANCHORED: `host` diganti hanya bila diikuti `:` atau `/`
# (posisi target jaringan). Karena itu `KC_DB: postgres` (dialek DB),
# `DB_SCHEME: postgresql`, dan `postgres:15` (tag image di compose) TIDAK
# ikut berubah.
# 3. `config/` sumber tidak pernah ditulis — hanya dibaca.
# 4. Baris KOMENTAR (`#`) tidak ditulis ulang: komentar sering menyebut nilai
# default lokal, dan menimpanya akan membuat komentar itu justru salah.
#
# PEMAKAIAN
# # Uji tanpa menulis (perlihatkan rencana):
# ./scripts/render-config.sh --out /tmp/render --dry-run \
# --map postgres=10.20.0.12 --map nessie=10.20.0.10
#
# # Render sungguhan:
# ./scripts/render-config.sh --out generated/host-compute/config \
# --map postgres=10.20.0.12 --map minio=10.20.0.12
#
# # Fase 1 akan menambah: --inventory cluster.yml --host <nama>
# ─────────────────────────────────────────────────────────────────────────────
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
DEPLOY_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"
SRC_CONFIG="${DEPLOY_DIR}/config"
OUT_DIR=""
DRY_RUN=0
declare -A MAP=()
# Hostname yang SAH dipetakan ke alamat host. Daftar sengaja tetap (bukan bebas)
# supaya salah ketik --map tidak diam-diam menulis ulang file config.
KNOWN_HOSTS=(
postgres minio nessie keycloak openldap opa
trino trino-coordinator trino-worker
starrocks-fe starrocks-cn airflow-webserver
openmetadata-server openmetadata-opensearch dremio grafana prometheus
# Sidecar observabilitas (M2/M3): target di config/prometheus dipetakan ke alamat
# HOST-nya saat multi-host. WAJIB ada di sini — generate.py meng-`--map` setiap
# RENDER_HOSTS, dan hostname yang tak terdaftar membuat render-config menolak,
# sehingga `install.sh --generate` GAGAL TOTAL (bug M2/M3 yang diperbaiki di sini).
statsd-exporter trino-jmx-exporter
)
# Tidak di-render: skrip yang sanggup ekspansi env sendiri + dokumentasi.
# environment.defaults.sh — di-source shell, memakai ${VAR:-...}
# buckets.sh — skrip mc, memakai ${MINIO_ENDPOINT:-...}
SKIP_FILES=(environment.defaults.sh buckets.sh)
SKIP_PATTERNS=("*.md")
RED=$'\e[31m'; GRN=$'\e[32m'; YLW=$'\e[33m'; BLD=$'\e[1m'; NC=$'\e[0m'
ok() { echo -e " ${GRN}[OK]${NC} $*"; }
inf() { echo -e " ${YLW}[..]${NC} $*"; }
err() { echo -e " ${RED}[!!]${NC} $*" >&2; }
usage() {
sed -n '2,30p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//'
echo
echo "Hostname yang dikenal: ${KNOWN_HOSTS[*]}"
}
is_known_host() {
local h="$1" k
for k in "${KNOWN_HOSTS[@]}"; do [[ "${k}" == "${h}" ]] && return 0; done
return 1
}
add_map() {
local pair="$1" host addr
if [[ "${pair}" != *"="* ]]; then
err "--map harus berformat host=alamat (diberikan: '${pair}')"; exit 2
fi
host="${pair%%=*}"; addr="${pair#*=}"
[[ -n "${host}" && -n "${addr}" ]] || { err "--map kosong: '${pair}'"; exit 2; }
is_known_host "${host}" || { err "hostname '${host}' tidak dikenal (lihat --help)"; exit 2; }
MAP["${host}"]="${addr}"
}
parse_args() {
while [[ $# -gt 0 ]]; do
case "$1" in
--out) OUT_DIR="${2:-}"; shift 2 ;;
--map) add_map "${2:-}"; shift 2 ;;
--dry-run) DRY_RUN=1; shift ;;
-h|--help) usage; exit 0 ;;
*) err "Argumen tidak dikenal: $1"; usage; exit 2 ;;
esac
done
[[ -n "${OUT_DIR}" ]] || { err "--out wajib diisi"; exit 2; }
[[ ${#MAP[@]} -gt 0 ]] || { err "minimal satu --map host=alamat wajib diisi"; exit 2; }
}
# Satu pass perl untuk semua host. Anchor diperketat ke TIGA konteks jaringan —
# tanpa ini, path filesystem seperti `/etc/trino/` atau `/data/trino/` ikut
# terkorupsi (hostname `trino` diikuti `/`):
# 1. setelah skema URL `//` → http://nessie:19120, jdbc:postgresql://postgres:5432
# 2. setelah `@` (JDBC) → user:pw@postgres:5432
# 3. bare `host:port` → nessie:19120, starrocks-fe:8030 (bukan bagian token lain)
# Baris komentar (`#`) tidak ditulis ulang.
render_file() {
local src="$1" dst="$2"
local -a pairs=()
local k
for k in "${!MAP[@]}"; do pairs+=("${k}=${MAP[$k]}"); done
perl -e '
my ($src, $dst, @pairs) = @ARGV;
my %map = map { split(/=/, $_, 2) } @pairs;
my $alt = join "|", sort { length($b) <=> length($a) } map { quotemeta } keys %map;
open(my $in, "<", $src) or die "buka gagal: $src\n";
open(my $out, ">", $dst) or die "tulis gagal: $dst\n";
while (my $l = <$in>) {
unless ($l =~ /^\s*#/) {
$l =~ s{(?<=//)(${alt})(?=[:/])}{$map{$1}}g; # 1. setelah skema //
$l =~ s{(?<=@)(${alt})(?=[:/])}{$map{$1}}g; # 2. setelah @ (JDBC)
$l =~ s{(?<![\w/.:-])(${alt})(?=:\d)}{$map{$1}}g; # 3. bare host:port
# 4. nilai BARE untuk key yang jelas host (host, hostname, *_HOST).
# `host: postgres` (Sling) tidak punya ':' atau '/', sehingga aturan 1-3
# tidak mengenainya -> di host lain Sling mencari nama container yang tidak
# ada (kelas bug yang sama dengan DB_HOST OpenMetadata). Hanya key ber-'host'
# yang disentuh, supaya `type: postgres` / `KC_DB: postgres` (dialek) aman.
$l =~ s{^(\s*(?:-\s*)?[A-Za-z_.\-]*(?:host|hostname)[A-Za-z_.\-]*\s*[:=]\s*)(${alt})(\s*)$}{$1 . $map{$2} . $3}gie;
}
print $out $l;
}
close $in; close $out;
' "${src}" "${dst}" "${pairs[@]}"
}
# Jumlah baris sumber yang berubah di file tujuan.
count_subs() {
local src="$1" dst="$2"
diff "${src}" "${dst}" 2>/dev/null | grep -c '^<' || true
}
should_skip() {
local name="$1" pat skip
for skip in "${SKIP_FILES[@]}"; do [[ "${name}" == "${skip}" ]] && return 0; done
for pat in "${SKIP_PATTERNS[@]}"; do
# shellcheck disable=SC2053
[[ "${name}" == ${pat} ]] && return 0
done
return 1
}
# Render satu file; cetak jumlah baris berubah bila ada.
process_file() {
local src="$1" rel="${1#${SRC_CONFIG}/}" dst="$2" n tmp
if [[ ${DRY_RUN} -eq 1 ]]; then
tmp="$(mktemp)"; render_file "${src}" "${tmp}"; n="$(count_subs "${src}" "${tmp}")"; rm -f "${tmp}"
else
dst="${OUT_DIR}/${rel}"
mkdir -p "$(dirname "${dst}")"
render_file "${src}" "${dst}"
n="$(count_subs "${src}" "${dst}")"
fi
CHANGED_TOTAL=$((CHANGED_TOTAL + n))
if [[ "${n}" -gt 0 ]]; then ok "${rel} — ${n} baris"; FILES_CHANGED=$((FILES_CHANGED + 1)); fi
}
main() {
parse_args "$@"
echo -e "${BLD}Render config/** untuk multi-host${NC}"
echo " sumber : ${SRC_CONFIG}"
echo " tujuan : ${OUT_DIR}$([[ ${DRY_RUN} -eq 1 ]] && echo ' (DRY-RUN)')"
echo -n " peta : "; for k in "${!MAP[@]}"; do printf '%s→%s ' "${k}" "${MAP[$k]}"; done; echo; echo
[[ ${DRY_RUN} -eq 0 ]] && mkdir -p "${OUT_DIR}"
FILES_CHANGED=0; CHANGED_TOTAL=0
local src scanned=0 skipped=0
while IFS= read -r -d '' src; do
scanned=$((scanned + 1))
if should_skip "$(basename "${src}")"; then skipped=$((skipped + 1)); continue; fi
process_file "${src}" ""
done < <(find "${SRC_CONFIG}" -type f -print0 | sort -z)
echo
inf "dipindai ${scanned} file · ${FILES_CHANGED} berubah (${CHANGED_TOTAL} baris) · ${skipped} dilewati"
echo -e " ${GRN}${BLD}Selesai.${NC} config/ sumber tidak tersentuh."
}
main "$@"