#!/usr/bin/env bash # ============================================================================= # eBdesk SiCABE — Verifikasi MinIO/S3: memastikan S3_ENDPOINT (url) di config # BENAR & bisa dipakai — dibuktikan dengan listing object nyata (contoh ≤5). # # TANPA mc: seluruh akses S3 memakai Python stdlib (SigV4 + urllib). # # Yang diperiksa: # A. Konfigurasi & endpoint : S3_ENDPOINT, endpoint host-facing, bucket, region, kredensial. # B. Kesehatan : GET /minio/health/live (dari host). # C. Akses S3 : ListBuckets (uji ENDPOINT + KREDENSIAL, SigV4). # D. Bucket & object : bucket ada? + contoh ≤5 object (Key+Size) + jumlah (maks 1000). # E. Ringkasan # # Pemakaian: # ./scripts/verify-minio.sh # ./scripts/verify-minio.sh --env-file generated/host-state/.env # S3_ENDPOINT=https://minio.corp:9000 S3_INSECURE=1 ./scripts/verify-minio.sh # Dukungan self-signed: set S3_INSECURE=1 (lewati verifikasi TLS). # Di belakang Cloudflare: User-Agent di-set agar tak diblokir Bot Fight Mode (error 1010); # timpa bila perlu: S3_UA='aws-sdk-go/1.44' ./scripts/verify-minio.sh # Keluar: 0 = OK (WARN boleh) · 1 = ada FAIL · 2 = argumen salah # ============================================================================= set -uo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" # shellcheck source=../lib/common.sh source "${SCRIPT_DIR}/lib/common.sh" 2>/dev/null || true # shellcheck source=../config/environment.defaults.sh source "${SCRIPT_DIR}/config/environment.defaults.sh" 2>/dev/null || true sicabe_load_env "${SCRIPT_DIR}" "$@" || exit 2 usage() { awk 'NR>1 && /^#/ {sub(/^# ?/,""); print; next} NR>1 {exit}' "$0"; } while [[ $# -gt 0 ]]; do case "$1" in --env-file) shift 2 ;; -h|--help) usage; exit 0 ;; *) echo "Argumen tidak dikenal: $1" >&2; exit 2 ;; esac done BOLD=$'\e[1m'; GREEN=$'\e[32m'; RED=$'\e[31m'; YELLOW=$'\e[33m'; CYAN=$'\e[36m'; NC=$'\e[0m' PASSC="${GREEN}[PASS]${NC}"; FAILC="${RED}[FAIL]${NC}"; WARNC="${YELLOW}[WARN]${NC}"; INFOC="${CYAN}[INFO]${NC}" PASS_N=0; FAIL_N=0; WARN_N=0 pass() { echo -e " ${PASSC} $1"; PASS_N=$((PASS_N + 1)); } fail() { echo -e " ${FAILC} $1"; FAIL_N=$((FAIL_N + 1)); } warn() { echo -e " ${WARNC} $1"; WARN_N=$((WARN_N + 1)); } info() { echo -e " ${INFOC} $1"; } # Penyebab singkat dari (kode HTTP, , pesan) — termasuk deteksi Cloudflare (error 1010). err_hint() { # local st="$1" code="$2" msg="$3" case "${msg}" in *1010*|*[Cc]loudflare*) echo "Cloudflare memblokir klien ini (Bot Fight Mode, error 1010) — set S3_UA, atau tambah exception/waf-skip di Cloudflare untuk host S3"; return ;; esac case "${st}" in 0) echo "tidak terjangkau (koneksi/DNS/TLS) — cek endpoint/port; self-signed? set S3_INSECURE=1";; 401) echo "butuh autentikasi";; 403) echo "kredensial/diakses ditolak${code:+ (${code})} — cek S3_ACCESS_KEY / S3_SECRET_KEY";; 404) echo "bucket/path tidak ada${code:+ (${code})}";; *) echo "${code:-HTTP ${st}}";; esac } # ── Konfigurasi (S3_ENDPOINT dilihat CONTAINER; skrip ini di HOST) ─────────── S3_ENDPOINT="${S3_ENDPOINT:-http://minio:9000}" S3_REGION="${S3_REGION:-us-east-1}" S3_ACCESS_KEY="${S3_ACCESS_KEY:-${MINIO_ROOT_USER:-minioadmin}}" S3_SECRET_KEY="${S3_SECRET_KEY:-${MINIO_ROOT_PASSWORD:-}}" BUCKET="${MINIO_BUCKET_ICEBERG_DATA:-iceberg-data}" # Endpoint HOST-facing: pakai S3_ENDPOINT bila menunjuk ALAMAT (IP/FQDN); # selain itu (nama service `minio`) → ://localhost: (port terbit). SCHEME="${S3_ENDPOINT%%://*}" _s3host="${S3_ENDPOINT#*://}"; _s3host="${_s3host%%/*}"; _s3host="${_s3host%%:*}" if [[ "${_s3host}" == *.* ]]; then S3_HOST_EP="${S3_ENDPOINT%/}" elif [[ "${MINIO_HOST:-}" == *.* ]]; then S3_HOST_EP="${SCHEME}://${MINIO_HOST}:${MINIO_PORT:-12900}" else S3_HOST_EP="${SCHEME}://localhost:${MINIO_PORT:-12900}" fi # ── Python stdlib: SigV4 + S3 (health / listbuckets / listobjects) ─────────── _PY="$(mktemp)"; trap 'rm -f "${_PY}"' EXIT cat > "${_PY}" <<'PYEOF' import os, sys, re, hashlib, hmac, datetime, ssl, urllib.request, urllib.parse, urllib.error mode = os.environ.get("MODE", "") ep = (os.environ.get("S3E") or "").rstrip("/") ak = os.environ.get("S3U") or "" sk = os.environ.get("S3P") or "" region = os.environ.get("S3R") or "us-east-1" bucket = os.environ.get("BUCKET") or "" insecure = os.environ.get("S3_INSECURE") == "1" # User-Agent: Cloudflare Bot Fight Mode MEMBLOKIR `Python-urllib/*` (error 1010) — # pakai UA yang jelas & tak masuk daftar bot (bisa ditimpa lewat S3_UA). ua = os.environ.get("S3_UA") or "sicabe-verify-minio/1.0 (+s3; path-style)" u = urllib.parse.urlparse(ep) scheme = u.scheme or "http" host = u.netloc base_path = u.path.rstrip("/") def ctx(): return ssl._create_unverified_context() if insecure else None def tags(xml, tag): return re.findall(rf"<{tag}>(.*?)", xml, re.S) def get(path, query=None): query = query or [] t = datetime.datetime.now(datetime.timezone.utc) amzdate = t.strftime("%Y%m%dT%H%M%SZ"); datestamp = t.strftime("%Y%m%d") curi = path or "/" cqs = "&".join(f"{urllib.parse.quote(k, safe='-_.~')}={urllib.parse.quote(str(v), safe='-_.~')}" for k, v in sorted(query)) phash = hashlib.sha256(b"").hexdigest() ch = f"host:{host}\nx-amz-content-sha256:{phash}\nx-amz-date:{amzdate}\n" sh = "host;x-amz-content-sha256;x-amz-date" cr = "\n".join(["GET", curi, cqs, ch, sh, phash]) scope = f"{datestamp}/{region}/s3/aws4_request" sts = "\n".join(["AWS4-HMAC-SHA256", amzdate, scope, hashlib.sha256(cr.encode()).hexdigest()]) def hm(k, m): return hmac.new(k, m.encode(), hashlib.sha256).digest() kk = hm(("AWS4" + sk).encode(), datestamp); kk = hm(kk, region); kk = hm(kk, "s3"); kk = hm(kk, "aws4_request") sig = hmac.new(kk, sts.encode(), hashlib.sha256).hexdigest() auth = f"AWS4-HMAC-SHA256 Credential={ak}/{scope}, SignedHeaders={sh}, Signature={sig}" url = f"{scheme}://{host}{curi}" + (f"?{cqs}" if cqs else "") req = urllib.request.Request(url, method="GET") for h, v in (("Host", host), ("x-amz-date", amzdate), ("x-amz-content-sha256", phash), ("Authorization", auth), ("User-Agent", ua)): req.add_header(h, v) try: with urllib.request.urlopen(req, timeout=15, context=ctx()) as r: return r.status, r.read().decode("utf-8", "replace") except urllib.error.HTTPError as e: return e.code, e.read().decode("utf-8", "replace") except Exception as e: return 0, str(e) def errline(st, body): c = tags(body, "Code"); m = tags(body, "Message") code = (c[0] if c else "").replace("|", " ") msg = (m[0] if m else body[:140]).replace("|", " ").strip() print(f"ERR|{st}|{code}|{msg}") if mode == "health": try: rq = urllib.request.Request(f"{scheme}://{host}/minio/health/live", method="GET") rq.add_header("User-Agent", ua) with urllib.request.urlopen(rq, timeout=8, context=ctx()) as r: print(f"HEALTH|{r.status}") except urllib.error.HTTPError as e: print(f"HEALTH|{e.code}") except Exception: print("HEALTH|000") elif mode == "buckets": st, body = get(base_path or "/") if st == 200: names = tags(body, "Name") print(f"BUCKETS|{len(names)}") for n in names: print(f"BUCKET|{n}") else: errline(st, body) elif mode == "objects": maxk = os.environ.get("MAXKEY", "5") st, body = get(f"{base_path}/{bucket}", [("list-type", "2"), ("max-keys", maxk)]) if st == 404 or "NoSuchBucket" in body: print("ERR|404|NoSuchBucket|bucket tak ada") elif st != 200: errline(st, body) else: keys = tags(body, "Key"); sizes = tags(body, "Size") trunc = "true" if "true" in body else "false" print(f"OK|keycount={len(keys)}|truncated={trunc}") for i, kk in enumerate(keys): # TAB-separated: key S3 boleh memuat '|' → jangan pakai '|' sebagai pemisah. print(f"OBJ\t{kk}\t{sizes[i] if i < len(sizes) else ''}") PYEOF s3py() { # [maxkey] → stdout MODE="$1" MAXKEY="${2:-}" S3E="${S3_HOST_EP}" S3U="${S3_ACCESS_KEY}" S3P="${S3_SECRET_KEY}" \ S3R="${S3_REGION}" BUCKET="${BUCKET}" S3_INSECURE="${S3_INSECURE:-0}" S3_UA="${S3_UA:-}" python3 "${_PY}" 2>/dev/null } echo -e "${BOLD}Verifikasi MinIO/S3 — bucket '${BUCKET}'${NC}" echo "" # ───────────────────────────────────────────────────────────────────────────── # A. Konfigurasi & endpoint # ───────────────────────────────────────────────────────────────────────────── echo -e "${BOLD}A. Konfigurasi & endpoint${NC}" info "S3_ENDPOINT (container) = ${S3_ENDPOINT}" info "endpoint (host, dipakai) = ${S3_HOST_EP}" info "bucket='${BUCKET}' region='${S3_REGION}' akses='${S3_ACCESS_KEY}'" if [[ -z "${S3_SECRET_KEY}" ]]; then fail "S3_SECRET_KEY / MINIO_ROOT_PASSWORD kosong — kredensial tak lengkap" fi echo "" # ───────────────────────────────────────────────────────────────────────────── # B. Kesehatan (host) # ───────────────────────────────────────────────────────────────────────────── echo -e "${BOLD}B. Kesehatan${NC}" hc="$(s3py health)" hc="${hc#HEALTH|}" if [[ "${hc}" == "200" ]]; then pass "MinIO sehat: ${S3_HOST_EP}/minio/health/live (HTTP 200)" elif [[ "${hc}" == "000" ]]; then warn "MinIO tak terjangkau dari host (${S3_HOST_EP}) — cek endpoint/port; self-signed? set S3_INSECURE=1" elif [[ "${hc}" =~ ^(2|3)[0-9][0-9]$ ]]; then info "endpoint merespon (HTTP ${hc}) — host HIDUP (redirect/OK); health diperiksa di bagian C/D bila perlu" else # 401/403/404: endpoint JELAS HIDUP (ada respons HTTP) — path health biasanya TIDAK # diekspos oleh proxy/HTTPS (mis. Traefik/Cloudflare hanya merutekan API S3). Bukan error. info "endpoint merespon (HTTP ${hc}) tetapi /minio/health/live tidak 200 — host HIDUP;" info "path health kemungkinan tak diekspos proxy/TLS → BUKAN kegagalan. Bukti utama: bagian C/D (S3 API)." fi echo "" # ───────────────────────────────────────────────────────────────────────────── # C. Akses S3 (ListBuckets) — uji ENDPOINT + KREDENSIAL (SigV4) # ───────────────────────────────────────────────────────────────────────────── echo -e "${BOLD}C. Akses S3 (ListBuckets)${NC}" bout="$(s3py buckets)" if [[ "${bout}" == BUCKETS\|* ]]; then nb="${bout#BUCKETS|}"; nb="${nb%%$'\n'*}" pass "ListBuckets OK via ${S3_HOST_EP} — ${nb} bucket" printf '%s\n' "${bout}" | awk -F'|' '$1=="BUCKET"{printf " • %s\n", $2}' elif [[ "${bout}" == ERR\|* ]]; then IFS='|' read -r _ st code msg <<< "${bout}" fail "ListBuckets GAGAL [HTTP ${st} — $(err_hint "${st}" "${code}" "${msg}")]${msg:+ · ${msg}}" else fail "ListBuckets tak menghasilkan keluaran yang diharapkan" fi echo "" # ───────────────────────────────────────────────────────────────────────────── # D. Bucket & object (contoh ≤5) # ───────────────────────────────────────────────────────────────────────────── echo -e "${BOLD}D. Bucket & object (contoh ≤5)${NC}" o5="$(s3py objects 5)" if [[ "${o5}" == OK\|* ]]; then pass "bucket '${BUCKET}' ada & bisa dibaca" meta="${o5%%$'\n'*}"; rest="${meta#*|}"; kc="${rest%%|*}" if [[ "${kc#keycount=}" -gt 0 ]]; then pass "menampilkan ≤5 object (bukti S3_ENDPOINT benar)" printf '%s\n' "${o5}" | awk -F'\t' '$1=="OBJ"{printf " • %s (%s bytes)\n", $2, $3}' else warn "bucket ada tetapi KOSONG (0 object) — normal bila belum ada ingest" fi # jumlah (maks 1000) — info o1000="$(s3py objects 1000)" if [[ "${o1000}" == OK\|* ]]; then meta="${o1000%%$'\n'*}"; r="${meta#*|}"; k1="${r%%|*}"; tr="${r##*|}" info "jumlah object (maks 1000): ${k1#keycount=}${tr:+ · ${tr}}" fi elif [[ "${o5}" == ERR\|* ]]; then IFS='|' read -r _ st code msg <<< "${o5}" if [[ "${st}" == "404" ]]; then fail "bucket '${BUCKET}' TIDAK ADA [HTTP 404 — ${msg:-NoSuchBucket}]" info "Buat bucket di sisi pemilik/MinIO (bootstrap step 2 / config/minio/buckets.sh)." else fail "baca bucket '${BUCKET}' GAGAL [HTTP ${st} — $(err_hint "${st}" "${code}" "${msg}")]${msg:+ · ${msg}}" fi else fail "baca bucket tak menghasilkan keluaran yang diharapkan" fi echo "" # ───────────────────────────────────────────────────────────────────────────── # E. Ringkasan # ───────────────────────────────────────────────────────────────────────────── echo -e "${BOLD}Ringkasan: ${PASS_N} PASS · ${FAIL_N} FAIL · ${WARN_N} WARN${NC}" if [[ ${FAIL_N} -eq 0 ]]; then echo -e "${GREEN}✓ S3_ENDPOINT valid & bisa dipakai (bucket '${BUCKET}').${NC}" exit 0 fi echo -e "${RED}✗ Ada ${FAIL_N} kegagalan — cek S3_ENDPOINT / kredensial / bucket.${NC}" exit 1