Files
platform-core/scripts/validate/admin-ui-smoke.sh
T

169 lines
7.0 KiB
Bash
Executable File

#!/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 2>/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 - <<EOF
apiVersion: v1
kind: Pod
metadata:
name: $pod
namespace: platform-admin
labels:
app.kubernetes.io/name: pgadmin4
app.kubernetes.io/instance: pgadmin
app.kubernetes.io/component: network-smoke
app.kubernetes.io/part-of: platform
spec:
automountServiceAccountToken: false
restartPolicy: Never
terminationGracePeriodSeconds: 1
securityContext:
runAsNonRoot: true
runAsUser: 65532
runAsGroup: 65532
seccompProfile:
type: RuntimeDefault
containers:
- name: network-smoke
image: $SMOKE_IMAGE
imagePullPolicy: IfNotPresent
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop:
- ALL
readOnlyRootFilesystem: true
command:
- sh
- -ec
- |
test "\$(nslookup id.learn.hyeonworks.com | awk '/^Address: /{print \$2}' | tail -n1)" = 192.168.0.107
test "\$(nslookup storage-admin.learn.hyeonworks.com | awk '/^Address: /{print \$2}' | tail -n1)" = 192.168.0.107
nc -z -w 5 platform-postgres-rw.platform-data.svc.cluster.local 5432
EOF
kubectl -n platform-admin wait --for=jsonpath='{.status.phase}'=Succeeded "pod/$pod" --timeout=90s
cleanup_pod
trap - EXIT INT TERM
if [[ "$run_s3" == true ]]; then
bash "$ROOT/scripts/validate/aistor-s3-smoke.sh" --execute
fi
printf 'ADMIN UI SMOKE PASS\n'
printf '남은 수동 검증: 허용/비허용 Keycloak 그룹 계정으로 두 UI 로그인\n'