Files
2026-10-09 02:35:14 +07:00

213 lines
10 KiB
Bash
Executable File

#!/usr/bin/env bash
# =============================================================================
# test-inventory.sh — uji validator inventory multi-host (Fase 1)
# -----------------------------------------------------------------------------
# Menguji `lib/cluster/inventory.py` secara negatif (harus DITOLAK) dan positif
# (harus DITERIMA). Sengaja banyak kasus negatif: validator yang selalu setuju
# tidak berguna — setiap aturan penempatan R10 §4.3 / R11 §3.2 diuji terpisah.
#
# Jalankan: ./scripts/test-inventory.sh
# =============================================================================
set -uo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
DEPLOY_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"
INV="${DEPLOY_DIR}/lib/cluster/inventory.py"
TMP="$(mktemp -d)"
trap 'rm -rf "${TMP}"' EXIT
GRN=$'\e[32m'; RED=$'\e[31m'; BLD=$'\e[1m'; NC=$'\e[0m'
PASS_N=0; FAIL_N=0
check() { # <harapan: reject|accept> <nama> <yaml>
local expect="$1" name="$2" yaml="$3" rc out
printf '%b' "$yaml" > "${TMP}/t.yml"
out="$(python3 "${INV}" validate "${TMP}/t.yml" 2>&1)"; rc=$?
if [[ "${expect}" == "reject" ]]; then
if [[ ${rc} -ne 0 && "${out}" != *"Traceback"* ]]; then
printf " ${GRN}✅ ditolak${NC} %-42s → %s\n" "${name}" "$(echo "${out}" | sed -n '2p' | cut -c1-66)"
PASS_N=$((PASS_N + 1))
else
printf " ${RED}❌ MASALAH${NC} %-42s rc=%s (harus ditolak tanpa traceback)\n" "${name}" "${rc}"
echo "${out}" | head -4 | sed 's/^/ /'
FAIL_N=$((FAIL_N + 1))
fi
else
if [[ ${rc} -eq 0 ]]; then
printf " ${GRN}✅ diterima${NC} %-41s\n" "${name}"
PASS_N=$((PASS_N + 1))
else
printf " ${RED}❌ DITOLAK (harusnya valid)${NC} %s\n" "${name}"
echo "${out}" | head -4 | sed 's/^/ /'
FAIL_N=$((FAIL_N + 1))
fi
fi
}
echo -e "${BLD}Uji validator inventory (lib/cluster/inventory.py)${NC}"
echo
echo "── harus DITOLAK ──"
check reject "state dipecah ke 2 host" 'hosts:\n a:\n address: 1.1.1.1\n tiers: [state]\n b:\n address: 2.2.2.2\n tiers: [state, engine]\n'
check reject "coord di 2 host" 'hosts:\n a:\n address: 1.1.1.1\n tiers: [coord, engine]\n b:\n address: 2.2.2.2\n tiers: [coord, state]\n'
check reject "state + engine satu host (anti-affinity)" 'hosts:\n a:\n address: 1.1.1.1\n tiers: [state, engine, coord]\n'
check reject "preset tak dikenal" 'preset: a9\nhosts:\n a:\n address: 1.1.1.1\n tiers: [state]\n b:\n address: 2.2.2.2\n tiers: [engine, coord]\n'
check reject "tier tak dikenal" 'hosts:\n a:\n address: 1.1.1.1\n tiers: [state, engine, coord, banjar]\n'
check reject "address kosong" 'hosts:\n a:\n tiers: [state, engine, coord]\n'
check reject "components.host tidak ada di hosts" 'hosts:\n a:\n address: 1.1.1.1\n tiers: [state]\n b:\n address: 2.2.2.2\n tiers: [engine, coord]\ncomponents:\n grafana: { host: tidak-ada }\n'
check reject "tanpa host bertier engine" 'hosts:\n a:\n address: 1.1.1.1\n tiers: [state, coord]\n'
check reject "hosts kosong" 'preset: a3\nhosts: {}\n'
check reject "YAML rusak" 'hosts:\n a: [ini: bukan: yaml\n'
echo
echo "── harus DITOLAK (TLS & domain) ──"
check reject "tls.mode tak dikenal" 'tls:
mode: tls
cluster:
domain: sicabe.com
preset: a3
hosts:
control: { address: 1.1.1.1, tiers: [identity, policy, pipeline, governance] }
compute: { address: 1.1.1.2, tiers: [coord, engine] }
state: { address: 1.1.1.3, tiers: [state] }
'
check reject "edge tanpa domain" 'tls:
mode: edge
preset: a3
hosts:
control: { address: 1.1.1.1, tiers: [identity, policy, pipeline, governance] }
compute: { address: 1.1.1.2, tiers: [coord, engine] }
state: { address: 1.1.1.3, tiers: [state] }
'
check reject "domain memuat skema" 'tls:
mode: edge
cluster:
domain: https://sicabe.com
preset: a3
hosts:
control: { address: 1.1.1.1, tiers: [identity, policy, pipeline, governance] }
compute: { address: 1.1.1.2, tiers: [coord, engine] }
state: { address: 1.1.1.3, tiers: [state] }
'
check reject "domain tanpa titik" 'tls:
mode: full
cluster:
domain: sicabe
preset: a3
hosts:
control: { address: 1.1.1.1, tiers: [identity, policy, pipeline, governance] }
compute: { address: 1.1.1.2, tiers: [coord, engine] }
state: { address: 1.1.1.3, tiers: [state] }
'
echo
echo "── harus DITERIMA ──"
check accept "cluster.example.yml (preset a3)" "$(cat "${DEPLOY_DIR}/cluster.example.yml")"
check accept "edge + domain sah" 'tls:
mode: edge
cluster:
domain: sicabe.com
preset: a3
hosts:
control: { address: 1.1.1.1, tiers: [identity, policy, pipeline, governance] }
compute: { address: 1.1.1.2, tiers: [coord, engine] }
state: { address: 1.1.1.3, tiers: [state] }
'
check accept "preset b5 (5 host, tanpa tiers eksplisit)" 'preset: b5\nhosts:\n control: { address: 10.0.0.10 }\n coord: { address: 10.0.0.11 }\n engine-1: { address: 10.0.0.12 }\n engine-2: { address: 10.0.0.13 }\n state: { address: 10.0.0.14 }\n'
echo
echo "── distribusi penempatan (plan) ──"
plan_out="$(python3 "${INV}" plan "${DEPLOY_DIR}/cluster.example.yml" 2>&1)"
plan_check() { # <nama> <host> <service-yang-harus-ada>
local name="$1" host="$2" svc="$3"
if echo "${plan_out}" | python3 -c "
import json,sys
p=json.load(sys.stdin)
sys.exit(0 if '$svc' in p.get('$host',{}).get('services',[]) else 1)"; then
printf " ${GRN}✅${NC} %-46s %s ∈ %s\n" "$name" "$svc" "$host"; PASS_N=$((PASS_N + 1))
else
printf " ${RED}❌${NC} %-46s %s TIDAK di %s\n" "$name" "$svc" "$host"; FAIL_N=$((FAIL_N + 1))
fi
}
plan_check "a3: coordinator di compute" compute trino-coordinator
plan_check "a3: worker di compute" compute trino-worker
plan_check "a3: postgres di state" state postgres
plan_check "a3: minio di state" state minio
plan_check "a3: keycloak di control" control keycloak
plan_check "a3: grafana di control" control grafana
plan_check "a3: airflow-webserver di control" control airflow-webserver
plan_not_in() { # <nama> <host> <service-yang-harus-TIDAK-ada>
local name="$1" host="$2" svc="$3"
if echo "${plan_out}" | python3 -c "
import json,sys
p=json.load(sys.stdin)
sys.exit(0 if '$svc' not in p.get('$host',{}).get('services',[]) else 1)"; then
printf " ${GRN}✅${NC} %-46s %s ∉ %s\n" "${name}" "${svc}" "${host}"; PASS_N=$((PASS_N + 1))
else
printf " ${RED}❌${NC} %-46s %s ADA di %s\n" "${name}" "${svc}" "${host}"; FAIL_N=$((FAIL_N + 1))
fi
}
plan_not_in "a3: postgres tidak di control" control postgres
plan_not_in "a3: trino-worker tidak di control" control trino-worker
plan_not_in "a3: dremio off (default) tidak ditempatkan" compute dremio
# b5: tier engine >1 host → worker & CN harus muncul di KEDUA host engine
printf 'preset: b5\nhosts:\n control: { address: 10.0.0.10 }\n coord: { address: 10.0.0.11 }\n engine-1: { address: 10.0.0.12 }\n engine-2: { address: 10.0.0.13 }\n state: { address: 10.0.0.14 }\n' > "${TMP}/b5.yml"
b5_out="$(python3 "${INV}" plan "${TMP}/b5.yml" 2>&1)"
b5_check() { # <host> <service>
if echo "${b5_out}" | python3 -c "
import json,sys
p=json.load(sys.stdin)
sys.exit(0 if '$2' in p.get('$1',{}).get('services',[]) else 1)"; then
printf " ${GRN}✅${NC} b5: %-14s %s\n" "$2 ∈ $1" ""; PASS_N=$((PASS_N + 1))
else
printf " ${RED}❌${NC} b5: %s TIDAK di %s\n" "$2" "$1"; FAIL_N=$((FAIL_N + 1))
fi
}
b5_check engine-1 trino-worker
b5_check engine-2 trino-worker
b5_check engine-1 starrocks-cn
b5_check engine-2 starrocks-cn
b5_check coord trino-coordinator
if echo "${b5_out}" | python3 -c "
import json,sys
p=json.load(sys.stdin)
sys.exit(0 if 'trino-worker' not in p.get('coord',{}).get('services',[]) else 1)"; then
printf " ${GRN}✅${NC} b5: trino-worker tidak di coord (coord ⊥ engine)\n"; PASS_N=$((PASS_N + 1))
else
printf " ${RED}❌${NC} b5: trino-worker ada di coord\n"; FAIL_N=$((FAIL_N + 1))
fi
echo "── URL browser-facing turunan (tls.mode) ──"
url_case() { # <nama> <yaml> <harapan: kosong|ada> <var>
local name="$1" yaml="$2" expect="$3" var="$4" out
printf '%b' "$yaml" > "${TMP}/tls.yml"
out="$(python3 -c "
import sys; sys.path.insert(0,'${DEPLOY_DIR}/lib/cluster')
import inventory
doc = inventory.apply_preset(inventory.load('${TMP}/tls.yml'))
print('|'.join(f'{k}={v}' for k,v in sorted(inventory.public_urls(doc).items())))" 2>/dev/null)"
if [[ "${expect}" == "kosong" ]]; then
if [[ -z "${out}" ]]; then printf " ${GRN}✅${NC} %s\n" "${name}"; PASS_N=$((PASS_N+1))
else printf " ${RED}❌${NC} %s — seharusnya kosong, dapat: %s\n" "${name}" "${out}"; FAIL_N=$((FAIL_N+1)); fi
else
if [[ "${out}" == *"${var}=${expect}"* ]]; then printf " ${GRN}✅${NC} %s\n" "${name}"; PASS_N=$((PASS_N+1))
else printf " ${RED}❌${NC} %s — tidak menemukan %s=%s di: %s\n" "${name}" "${var}" "${expect}" "${out}"; FAIL_N=$((FAIL_N+1)); fi
fi
}
A3='tls:\n mode: none\ncluster:\n domain: sicabe.com\npreset: a3\nhosts:\n control: { address: 1.1.1.1, tiers: [identity, policy, pipeline, governance] }\n compute: { address: 1.1.1.2, tiers: [coord, engine] }\n state: { address: 1.1.1.3, tiers: [state] }\n'
EDGE='tls:\n mode: edge\ncluster:\n domain: sicabe.com\npreset: a3\nhosts:\n control: { address: 1.1.1.1, tiers: [identity, policy, pipeline, governance] }\n compute: { address: 1.1.1.2, tiers: [coord, engine] }\n state: { address: 1.1.1.3, tiers: [state] }\n'
FULL='tls:\n mode: full\ncluster:\n domain: contoh.org\npreset: a3\nhosts:\n control: { address: 1.1.1.1, tiers: [identity, policy, pipeline, governance] }\n compute: { address: 1.1.1.2, tiers: [coord, engine] }\n state: { address: 1.1.1.3, tiers: [state] }\n'
url_case "mode none -> TIDAK ada URL publik" "${A3}" kosong ""
url_case "mode edge -> Keycloak https" "${EDGE}" "https://kc.sicabe.com" "KEYCLOAK_PUBLIC_URL"
url_case "mode edge -> Grafana https" "${EDGE}" "https://grafana.sicabe.com" "GRAFANA_PUBLIC_URL"
url_case "mode edge -> MinIO console https" "${EDGE}" "https://minio.sicabe.com" "MINIO_CONSOLE_PUBLIC_URL"
url_case "mode full -> memakai domain sendiri" "${FULL}" "https://om.contoh.org" "OPENMETADATA_PUBLIC_URL"
echo
if [[ ${FAIL_N} -eq 0 ]]; then
echo -e " ${GRN}${BLD}SEMUA LULUS${NC} (${PASS_N} cek)"
exit 0
fi
echo -e " ${RED}${BLD}GAGAL${NC}: ${FAIL_N} bermasalah · ${PASS_N} lulus"
exit 1