Files
service-catalog/images/keycloak/verify.sh
T
wbsong111 a27c2a9423 add-keycloakx: images/keycloak 의 셸 스크립트에 실행 권한 부여
기존 3종의 verify.sh 가 전부 100755 인데 100644 로 들어갔다. 둘 다 `bash <파일>`
로 호출되어 동작에는 영향이 없지만 관례를 맞춘다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 13:50:36 +09:00

200 lines
9.1 KiB
Bash
Executable File

#!/usr/bin/env bash
# keycloak 이미지 기능 검증 — 호스트에서 bash 로 실행된다(build-hardened-image.sh 가
# `env TAG=... PLATFORM=... <build.env 의 모든 변수> bash verify.sh` 로 호출한다).
#
# 마지막 줄에 VERIFY-OK 를 출력하면 통과다.
#
# 이 이미지에 요구하는 것
# 1. kc.sh 가 의존하는 셸 도구(sed·grep·readlink·dirname·uname)와 java 21
# — bci-micro 에는 sed 가 없어 RUNTIME_PACKAGES 로 명시 설치한다
# 2. 로케일(en_US.UTF-8)·타임존(custom-values.yaml 이 TZ=Asia/Seoul 을 넣는다)
# 3. 오버레이한 jar 가 **내용상** 새 버전일 것
# — overlay-jars.sh 가 파일명을 유지하므로 파일명으로는 확인할 수 없다.
# trivy 가 보는 것과 같은 근거(jar 내부 META-INF/maven/**/pom.properties)로 확인한다.
# 4. bin/client 제거 확인 (overlay-jars.sh 가 지운다)
# 5. 실제 기동 — realm 이 만들어지고 admin 토큰이 발급될 것
# (게이트 0건이어도 못 쓰는 이미지는 무의미하다 — .claude/image-authoring.md 5번)
set -e
TAG="${TAG:?TAG 환경변수가 필요하다}"
PLATFORM="${PLATFORM:-linux/amd64}"
KEYCLOAK_VERSION="${KEYCLOAK_VERSION:?build.env 에서 전달돼야 한다}"
NETTY_OLD="${NETTY_OLD:?}" ; NETTY_VERSION="${NETTY_VERSION:?}"
JACKSON_OLD="${JACKSON_OLD:?}" ; JACKSON_VERSION="${JACKSON_VERSION:?}"
PGJDBC_OLD="${PGJDBC_OLD:?}" ; PGJDBC_VERSION="${PGJDBC_VERSION:?}"
WORK="$(mktemp -d)"
CID=""
CCID=""
cleanup() {
[ -n "$CID" ] && docker rm -f "$CID" >/dev/null 2>&1 || true
[ -n "$CCID" ] && docker rm -f "$CCID" >/dev/null 2>&1 || true
rm -rf "$WORK"
}
trap cleanup EXIT
LIBDIR=/opt/keycloak/lib/lib/main
# ------------------------------------------------------------------ 1~2, 4단계
docker run --rm -i --platform "$PLATFORM" -e TZ=Asia/Seoul \
-e NETTY_OLD="$NETTY_OLD" --entrypoint sh "$TAG" <<'GUEST'
set -e
LIB=/opt/keycloak/lib/lib/main
echo "== kc.sh 가 쓰는 셸 도구 =="
for b in sh bash sed grep readlink dirname uname java; do
p=$(command -v "$b") || { echo "FAIL: $b 를 PATH 에서 찾을 수 없다"; exit 1; }
echo " $b -> $p"
done
echo "== java 21 =="
VER="$(java -version 2>&1 | head -1)"
echo " $VER"
case "$VER" in
*'"21'*) ;;
*) echo "FAIL: java 21 이 아니다"; exit 1 ;;
esac
echo "== 로케일 / 타임존 =="
CHARMAP="$(locale charmap 2>/dev/null || echo '?')"
echo " LANG=$LANG charmap=$CHARMAP"
[ "$CHARMAP" = "UTF-8" ] || { echo "FAIL: en_US.UTF-8 로케일이 없다 (glibc-locale-base 확인)"; exit 1; }
TZNAME="$(date +%Z)"
echo " TZ=Asia/Seoul -> $TZNAME"
[ "$TZNAME" = "KST" ] || { echo "FAIL: tzdata 가 Asia/Seoul 을 모른다 (got: $TZNAME)"; exit 1; }
echo "== bin/client 제거 확인 =="
# jackson 을 shade 로 품은 uber-jar 라 jar 교체로 못 고쳐 통째로 제거했다.
if [ -e /opt/keycloak/bin/client ]; then
echo "FAIL: bin/client 가 남아 있다 — overlay-jars.sh 의 제거 단계가 동작하지 않았다"; exit 1
fi
echo " 없음 (정상)"
echo "== 배포본 레이아웃 =="
[ -d "$LIB" ] || { echo "FAIL: $LIB 없음"; exit 1; }
n=0; for f in "$LIB"/io.netty.*-"$NETTY_OLD"*.jar; do [ -e "$f" ] && n=$((n+1)); done
echo " netty jar 파일 ${n}개 (파일명은 유지되는 것이 정상 — 내용 검증은 호스트에서)"
[ "$n" -gt 0 ] || { echo "FAIL: netty jar 가 없다"; exit 1; }
GUEST
# ------------------------------------------------------------------ 3단계
# 파일명을 유지하는 설계라 파일명으로는 교체 여부를 알 수 없다. jar 를 호스트로 꺼내
# 내부 pom.properties 를 읽는다 — trivy 가 버전을 판정하는 것과 같은 근거다.
# (게스트에 unzip 을 넣지 않기 위해 호스트 python3 로 검사한다. build-hardened-image.sh
# 가 이미 python3 를 전제한다.)
echo "== 오버레이 jar 내용 검증 (jar 내부 메타데이터) =="
CCID="$(docker create --platform "$PLATFORM" "$TAG")"
check_jar() { # $1=컨테이너 내 파일명 $2=기대 버전
local name="$1" want="$2" got
docker cp "$CCID:$LIBDIR/$name" "$WORK/$name" >/dev/null
got="$(python3 - "$WORK/$name" <<'PY'
import sys, zipfile, re
# 1순위: META-INF/maven/**/pom.properties (netty·jackson 등 대부분)
# 2순위: MANIFEST.MF 의 Bundle-Version / Implementation-Version
# (pgjdbc 는 pom.properties 를 넣지 않고 OSGi Bundle-Version 만 쓴다 — 실측)
with zipfile.ZipFile(sys.argv[1]) as z:
for n in z.namelist():
if re.fullmatch(r'META-INF/maven/[^/]+/[^/]+/pom\.properties', n):
for line in z.read(n).decode().splitlines():
if line.startswith('version='):
print(line.split('=', 1)[1].strip()); sys.exit(0)
try:
mf = z.read('META-INF/MANIFEST.MF').decode('utf-8', 'replace')
except KeyError:
print('NOT-FOUND'); sys.exit(0)
# MANIFEST 는 72바이트에서 줄바꿈 후 다음 줄을 한 칸 들여쓰기로 이어붙인다
mf = mf.replace('\r\n', '\n').replace('\n ', '')
for key in ('Bundle-Version', 'Implementation-Version'):
m = re.search(rf'^{key}:\s*(\S+)\s*$', mf, re.M)
if m:
print(m.group(1)); sys.exit(0)
print('NOT-FOUND')
PY
)"
if [ "$got" != "$want" ]; then
echo "FAIL: $name 의 내부 버전이 '$got' 다 (기대 '$want') — 오버레이가 적용되지 않았다"
exit 1
fi
echo " $name -> 내장 버전=$got"
}
check_jar "io.netty.netty-codec-http-${NETTY_OLD}.jar" "$NETTY_VERSION"
check_jar "io.netty.netty-codec-${NETTY_OLD}.jar" "$NETTY_VERSION"
check_jar "com.fasterxml.jackson.core.jackson-databind-${JACKSON_OLD}.jar" "$JACKSON_VERSION"
check_jar "com.fasterxml.jackson.core.jackson-core-${JACKSON_OLD}.jar" "$JACKSON_VERSION"
check_jar "org.postgresql.postgresql-${PGJDBC_OLD}.jar" "$PGJDBC_VERSION"
docker rm -f "$CCID" >/dev/null 2>&1 || true
CCID=""
# ------------------------------------------------------------------ 5단계
echo "== 실기동 (start-dev, 내장 dev-file DB) =="
ADMIN_USER="verify-admin"
ADMIN_PASS="verify-$$-$RANDOM"
# 호스트 포트는 커널이 고르게 한다(CI 러너에서 고정 포트 충돌을 피한다).
CID="$(docker run -d --platform "$PLATFORM" \
-p 127.0.0.1::8080 \
-e KC_BOOTSTRAP_ADMIN_USERNAME="$ADMIN_USER" \
-e KC_BOOTSTRAP_ADMIN_PASSWORD="$ADMIN_PASS" \
-e TZ=Asia/Seoul \
"$TAG" start-dev)"
HOSTPORT="$(docker port "$CID" 8080/tcp | head -1 | rev | cut -d: -f1 | rev)"
BASE="http://127.0.0.1:${HOSTPORT}"
echo " container=${CID:0:12} base=$BASE"
# 기본값이 넉넉한 이유: arm64 호스트에서 linux/amd64 를 QEMU 로 돌리면 Quarkus
# augmentation 만 ~100초, 기동 전체가 ~300초를 넘는다(2026-08-07 실측 — 180초로는
# liquibase 스키마 생성 중에 잘렸다). 네이티브 amd64 러너에서는 1~2분이면 끝나고,
# 루프는 뜨는 즉시 빠져나오므로 큰 값이 느려지는 비용은 없다.
BOOT_TIMEOUT="${VERIFY_BOOT_TIMEOUT:-600}"
echo "== master realm 기동 대기 (최대 ${BOOT_TIMEOUT}s) =="
ok=0
for i in $(seq 1 "$BOOT_TIMEOUT"); do
if curl -fsS "$BASE/realms/master/.well-known/openid-configuration" >"$WORK/disco.json" 2>/dev/null; then
ok=1; echo " ${i}s 만에 응답"; break
fi
if [ -z "$(docker ps -q --filter "id=$CID")" ]; then
echo "FAIL: 컨테이너가 죽었다"; docker logs "$CID" 2>&1 | tail -40; exit 1
fi
# 30초마다 어디까지 갔는지 남긴다 — 느린 것과 멈춘 것을 로그로 구분하기 위함이다.
if [ $((i % 30)) -eq 0 ]; then
echo " ...${i}s: $(docker logs "$CID" 2>&1 | tail -1 | cut -c1-140)"
fi
sleep 1
done
if [ "$ok" != 1 ]; then
echo "FAIL: ${BOOT_TIMEOUT}초 내에 OIDC discovery 가 응답하지 않았다"
docker logs "$CID" 2>&1 | tail -60
exit 1
fi
ISSUER="$(python3 -c 'import json,sys;print(json.load(open(sys.argv[1]))["issuer"])' "$WORK/disco.json")"
echo " issuer=$ISSUER"
echo "== admin 토큰 발급 (부트스트랩 계정 + DB 마이그레이션 확인) =="
curl -fsS -X POST "$BASE/realms/master/protocol/openid-connect/token" \
-d "client_id=admin-cli" -d "grant_type=password" \
-d "username=$ADMIN_USER" --data-urlencode "password=$ADMIN_PASS" \
> "$WORK/token.json"
python3 - "$WORK/token.json" <<'PY'
import json, sys
tok = json.load(open(sys.argv[1]))
assert tok.get("access_token"), f"access_token 없음: {tok}"
print(f" access_token 발급 OK (expires_in={tok.get('expires_in')}s)")
PY
echo "== admin REST API 호출 (realms 목록) =="
AT="$(python3 -c 'import json,sys;print(json.load(open(sys.argv[1]))["access_token"])' "$WORK/token.json")"
curl -fsS -H "Authorization: Bearer $AT" "$BASE/admin/realms" > "$WORK/realms.json"
python3 - "$WORK/realms.json" <<'PY'
import json, sys
realms = [r["realm"] for r in json.load(open(sys.argv[1]))]
assert "master" in realms, f"master realm 없음: {realms}"
print(f" realms={realms}")
PY
# netty/jackson 오버레이가 실제 HTTP 스택에서 동작했다는 증거 — 위 요청들이 전부
# Quarkus(netty) 위에서 처리되고 jackson 으로 직렬화된 JSON 이다.
echo "VERIFY-OK"