169 lines
7.0 KiB
Bash
Executable File
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'
|