#!/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-/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 # ───────────────────────────────────────────────────────────────────────────── 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{(? 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 "$@"