#!/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() { # 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() { # 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() { # 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() { # 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() { # 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