#!/usr/bin/env bash set -Eeuo pipefail set +x readonly ROOT="$(cd -- "$(dirname -- "$BASH_SOURCE")/../.." && pwd -P)" readonly LAN_IP=192.168.0.107 readonly TAIL_IP=100.92.240.34 readonly STORAGE_HOST=storage-admin.learn.hyeonworks.com readonly DB_HOST=db-admin.learn.hyeonworks.com readonly SMOKE_IMAGE="busybox@sha256:7a3ebe5bfd1a4a19797d20b0c0bb39d44393e9a03fd852c0865b0f540d868df0" execute=false run_s3=false fail() { printf 'ERROR: %s\n' "$*" >&2 exit 1 } usage() { cat <<'USAGE' 사용법: bash scripts/validate/admin-ui-smoke.sh bash scripts/validate/admin-ui-smoke.sh --execute [--run-s3] 기본 실행은 공개 DNS, 인증서, 리소스, Secret key 이름만 읽습니다. --execute는 LAN/Tailscale/Pod DNS와 실제 HTTPS·NetworkPolicy를 검사하는 임시 Pod를 만들었다가 삭제합니다. --run-s3는 기존 인증 S3 회귀 검사도 연결하며 그 스크립트의 APPLY 확인을 한 번 더 요구합니다. USAGE } while (( $# > 0 )); do case "$1" in --execute) execute=true; shift ;; --run-s3) run_s3=true; shift ;; -h|--help) usage; exit 0 ;; *) usage >&2; fail "지원하지 않는 인자: $1" ;; esac done [[ "$run_s3" == false || "$execute" == true ]] || fail "--run-s3에는 --execute가 필요합니다" [[ "$(pwd -P)" == "$ROOT" ]] || fail "$ROOT에서 실행하세요" for cmd in curl dig jq kubectl nc openssl rg sort tr; do command -v "$cmd" >/dev/null 2>&1 || fail "$cmd 명령이 필요합니다" done for host in "$STORAGE_HOST" "$DB_HOST"; do [[ -z "$(dig +short @1.1.1.1 A "$host" | tr -d '[:space:]')" ]] || fail "$host 공개 A가 있습니다" [[ -z "$(dig +short @1.1.1.1 AAAA "$host" | tr -d '[:space:]')" ]] || fail "$host 공개 AAAA가 있습니다" done kubectl -n object-storage get objectstore minio-aistor -o json | \ jq -e '.status.healthStatus == "green"' >/dev/null || fail "AIStor가 green이 아닙니다" kubectl -n platform-admin get deployment pgadmin -o json | \ jq -e '.status.availableReplicas == 1 and .spec.strategy.type == "Recreate"' >/dev/null || \ fail "pgAdmin Deployment가 available/Recreate 상태가 아닙니다" kubectl -n platform-admin get pvc pgadmin -o json | \ jq -e '.status.phase == "Bound" and .spec.resources.requests.storage == "2Gi"' >/dev/null || \ fail "pgAdmin PVC가 2Gi Bound가 아닙니다" kubectl get pv pgadmin-data-local-pv -o json | \ jq -e '.spec.persistentVolumeReclaimPolicy == "Retain"' >/dev/null || fail "pgAdmin PV가 Retain이 아닙니다" for contract in "object-storage aistor-keycloak-oidc" "platform-admin pgadmin-keycloak-oidc"; do set -- $contract keys="$(kubectl -n "$1" get "secret/$2" \ -o go-template='{{range $key, $_ := .data}}{{$key}}{{"\n"}}{{end}}' | LC_ALL=C sort)" [[ "$keys" == $'client-id\nclient-secret' ]] || fail "$1/$2 Secret 계약이 다릅니다" done keys="$(kubectl -n platform-admin get secret pgadmin-bootstrap \ -o go-template='{{range $key, $_ := .data}}{{$key}}{{"\n"}}{{end}}')" [[ "$keys" == password ]] || fail "pgAdmin bootstrap Secret 계약이 다릅니다" config="$(kubectl -n platform-admin get configmap pgadmin-config -o jsonpath='{.data.config_local\.py}')" printf '%s\n' "$config" | rg -q -F 'ALLOW_SAVE_PASSWORD = False' || fail "비밀번호 저장 차단이 없습니다" printf '%s\n' "$config" | rg -q -F "'groups': ['/platform-db-admins']" || fail "pgAdmin 그룹 gate가 없습니다" kubectl -n platform-data exec platform-postgres-1 -c postgres -- \ psql -Atqc "SELECT count(*) FROM pg_roles WHERE rolname IN ('pgadmin','platform-dba') OR (rolsuper AND rolname NOT IN ('postgres','streaming_replica'));" \ | rg -q '^0$' || fail "예상하지 못한 pgAdmin/DBA/superuser 역할이 있습니다" cert="$(openssl s_client -connect 127.0.0.1:443 -servername "$STORAGE_HOST" /dev/null | \ openssl x509 -noout -text)" printf '%s\n' "$cert" | rg -q "DNS:$STORAGE_HOST" || fail "admin 인증서 SAN이 없습니다" printf '%s\n' "$cert" | rg -q "DNS:$DB_HOST" || fail "admin 인증서 SAN이 없습니다" printf 'READ-ONLY CHECK PASS: 공개 admin DNS 없음, 리소스·Secret 계약·인증서 정상\n' if [[ "$execute" == false ]]; then printf '%s\n' '--execute를 지정하지 않아 임시 Pod와 네트워크 요청은 만들지 않았습니다.' exit 0 fi [[ -t 0 ]] || fail "--execute는 대화형 터미널이 필요합니다" printf 'Type APPLY to run transient DNS and network smoke tests: ' read -r answer [[ "$answer" == APPLY ]] || fail "취소했습니다" for pair in "$LAN_IP $STORAGE_HOST" "$LAN_IP $DB_HOST" "$TAIL_IP $STORAGE_HOST" "$TAIL_IP $DB_HOST"; do set -- $pair [[ "$(dig +short "@$1" A "$2" | tail -n1)" == "$1" ]] || fail "$2 private DNS 실패" done for host in "$STORAGE_HOST" "$DB_HOST"; do code="$(curl --silent --show-error --output /dev/null --write-out '%{http_code}' \ --noproxy '*' --interface "$LAN_IP" --resolve "$host:443:$LAN_IP" "https://$host/")" [[ "$code" == 200 || "$code" == 302 || "$code" == 303 ]] || fail "$host LAN HTTPS 실패: $code" denied="$(curl --silent --show-error --output /dev/null --write-out '%{http_code}' \ --noproxy '*' --resolve "$host:443:127.0.0.1" "https://$host/")" [[ "$denied" == 403 ]] || fail "$host 허용 목록 밖 요청이 403이 아닙니다" done if nc -z -w 2 "$LAN_IP" 30080 || nc -z -w 2 "$LAN_IP" 30443; then fail "Traefik NodePort가 LAN 주소에 노출됐습니다" fi nc -z -w 2 127.0.0.1 30080 || fail "Traefik HTTP NodePort loopback이 닫혔습니다" pod="admin-ui-smoke-$(date +%H%M%S)" cleanup_pod() { kubectl -n platform-admin delete "pod/$pod" --ignore-not-found --wait=false >/dev/null 2>&1 || true } trap cleanup_pod EXIT INT TERM kubectl apply -f - <