Files

244 lines
8.7 KiB
Bash
Executable File

#!/usr/bin/env bash
set -Eeuo pipefail
readonly EXPECTED_HELM_VERSION="v3.19.4"
readonly EXPECTED_KUSTOMIZE_VERSION="v5.8.1"
readonly KUBE_VERSION="1.36.2"
readonly PGADMIN_SHA="0f409becdc6a811e852f384cb53b3f11cfb652a19d77cb952a8a176071694f4c"
readonly AISTOR_SHA="50ffa6a4e014cdc48566b237593baab41d039f103cdc50c1b1ac173b8d8bf71e"
readonly IMAGE_SHA="sha256:66a300a7ecdcc1f325af0c430315329bca46cd4a7067227d6899802238167c6e"
readonly ROOT="$(cd -- "$(dirname -- "$BASH_SOURCE")/../.." && pwd -P)"
readonly PG_CACHE="$ROOT/services/pgadmin/.helm/charts/pgadmin4-helm-9.16.0"
readonly AI_CACHE="$ROOT/services/minio-aistor/base/deployment/.helm/charts/aistor-objectstore-1.0.16"
readonly NAMES="admin-namespace pgadmin-local-pv coredns-custom aistor-admin-oidc pgadmin"
fail() {
printf 'ERROR: %s\n' "$*" >&2
exit 1
}
usage() {
cat <<'USAGE'
사용법:
PLATFORM_HELM_BIN=/home/donghyeon/.local/bin/helm \
bash scripts/validate/render-admin-services.sh
적용 스크립트 내부 handoff:
bash scripts/validate/render-admin-services.sh \
--verified-output-dir /tmp/platform-admin-apply.XXXXXX
고정한 공식 차트의 SHA-256과 렌더 결과를 검사합니다. 변경은 수행하지 않습니다.
USAGE
}
out=""
if (( $# == 0 )); then
:
elif (( $# == 2 )) && [[ "$1" == "--verified-output-dir" ]]; then
out="$2"
else
usage >&2
exit 2
fi
for cmd in awk find helm install kubectl rg sed sha256sum stat tar wc tr; do
if [[ "$cmd" == helm ]] && [[ -v PLATFORM_HELM_BIN ]]; then
continue
fi
command -v "$cmd" >/dev/null 2>&1 || fail "$cmd 명령이 필요합니다"
done
if [[ -v PLATFORM_HELM_BIN ]]; then
[[ "$PLATFORM_HELM_BIN" == /* && -x "$PLATFORM_HELM_BIN" ]] || \
fail "PLATFORM_HELM_BIN은 실행 가능한 절대 경로여야 합니다"
readonly HELM="$PLATFORM_HELM_BIN"
else
readonly HELM="$(command -v helm)"
fi
[[ "$("$HELM" version --template '{{.Version}}')" == "$EXPECTED_HELM_VERSION" ]] || \
fail "Helm은 정확히 $EXPECTED_HELM_VERSION이어야 합니다"
kv="$(kubectl version --client --output=yaml | sed -n 's/^kustomizeVersion: //p')"
[[ "$kv" == "$EXPECTED_KUSTOMIZE_VERSION" ]] || \
fail "Kustomize는 정확히 $EXPECTED_KUSTOMIZE_VERSION이어야 합니다"
if [[ -n "$out" ]]; then
[[ "$out" == /tmp/platform-admin-apply.* ]] || fail "handoff 경로 형식이 잘못됐습니다"
[[ -d "$out" && ! -L "$out" && -O "$out" ]] || fail "handoff 경로가 안전하지 않습니다"
[[ "$(stat --format='%a' "$out")" == 700 ]] || fail "handoff 권한은 0700이어야 합니다"
[[ -z "$(find "$out" -mindepth 1 -maxdepth 1 -print -quit)" ]] || fail "handoff 경로가 비어 있지 않습니다"
fi
umask 077
work="$(mktemp -d /tmp/platform-admin-render.XXXXXX)"
pg_created=false
ai_created=false
cleanup() {
rc=$?
set +e
[[ "$pg_created" == true ]] && rm -rf -- "$PG_CACHE"
[[ "$ai_created" == true ]] && rm -rf -- "$AI_CACHE"
rmdir -- "$ROOT/services/pgadmin/.helm/charts" "$ROOT/services/pgadmin/.helm" 2>/dev/null || true
rmdir -- "$ROOT/services/minio-aistor/base/deployment/.helm/charts" \
"$ROOT/services/minio-aistor/base/deployment/.helm" 2>/dev/null || true
case "$work" in
/tmp/platform-admin-render.*) rm -rf -- "$work" ;;
esac
exit "$rc"
}
trap cleanup EXIT
trap 'exit 130' INT
trap 'exit 143' TERM
prepare() {
label="$1"
ref="$2"
name="$3"
version="$4"
expected="$5"
cache="$6"
archive="$work/$name-$version.tgz"
[[ ! -e "$cache" && ! -L "$cache" ]] || fail "생성 캐시가 이미 있습니다: $cache"
if [[ "$ref" == oci://* ]]; then
"$HELM" pull "$ref" --version "$version" --destination "$work"
else
"$HELM" pull "$name" --repo "$ref" --version "$version" --destination "$work"
fi
[[ -f "$archive" ]] || fail "$label archive가 없습니다"
actual="$(sha256sum "$archive" | awk '{print $1}')"
[[ "$actual" == "$expected" ]] || fail "$label SHA-256 불일치: $actual"
mkdir -p -- "$cache"
[[ "$cache" == "$PG_CACHE" ]] && pg_created=true
[[ "$cache" == "$AI_CACHE" ]] && ai_created=true
tar -xzf "$archive" -C "$cache"
[[ -f "$cache/$name/Chart.yaml" ]] || fail "$label Chart.yaml이 없습니다"
printf '검증 %-18s SHA-256 %s\n' "$label" "$actual"
}
plain() {
kubectl kustomize "$ROOT/$2" >"$work/$1.yaml"
[[ -s "$work/$1.yaml" ]] || fail "$1 렌더 결과가 비었습니다"
}
with_helm() {
kubectl kustomize --enable-helm --helm-command "$HELM" \
--helm-kube-version "$KUBE_VERSION" "$ROOT/$2" >"$work/$1.yaml"
[[ -s "$work/$1.yaml" ]] || fail "$1 렌더 결과가 비었습니다"
}
cd -- "$ROOT"
if rg -n --glob '*.yaml' --glob '*.yml' --glob '!**/.helm/**' \
'^[[:space:]]*kind:[[:space:]]*Secret[[:space:]]*$' infrastructure services bootstrap; then
fail "Git 관리 대상에서 Secret manifest를 발견했습니다"
fi
if rg -n '^[[:space:]]*hostAliases:' services/gitea/profiles/oidc; then
fail "Gitea hostAliases가 남아 있습니다"
fi
prepare pgadmin oci://docker.io/dpage/pgadmin4-helm pgadmin4-helm 9.16.0 "$PGADMIN_SHA" "$PG_CACHE"
prepare aistor-objectstore https://helm.min.io/ aistor-objectstore 1.0.16 "$AISTOR_SHA" "$AI_CACHE"
plain admin-namespace infrastructure/namespaces/admin-tools
plain pgadmin-local-pv infrastructure/storage/pgadmin-local-pv
plain coredns-custom infrastructure/networking/private-dns/kubernetes
with_helm aistor-admin-oidc services/minio-aistor/profiles/admin-oidc
with_helm pgadmin services/pgadmin
if rg -n '^[[:space:]]*kind:[[:space:]]*Secret[[:space:]]*$' "$work"/*.yaml; then
fail "렌더 결과에 Secret 리소스가 있습니다"
fi
[[ "$(rg -c -F "image: docker.io/dpage/pgadmin4@$IMAGE_SHA" "$work/pgadmin.yaml")" == 3 ]] || \
fail "pgAdmin main/init 이미지 세 개의 digest 고정이 깨졌습니다"
rg -q -F 'type: Recreate' "$work/pgadmin.yaml" || fail "pgAdmin Recreate 전략이 없습니다"
rg -q -F 'storageClassName: ssd-local-pgadmin-retain' "$work/pgadmin.yaml" || fail "pgAdmin StorageClass가 다릅니다"
[[ "$(rg -c -F 'value: db-admin.learn.hyeonworks.com' "$work/pgadmin.yaml")" == 3 ]] || \
fail "pgAdmin 세 probe의 Host header가 고정되지 않았습니다"
rg -q -F 'host: db-admin.learn.hyeonworks.com' "$work/pgadmin.yaml" || fail "pgAdmin Ingress가 없습니다"
[[ "$(rg -c -F 'ingressClassName: traefik' "$work/pgadmin.yaml")" == 1 ]] || \
fail "pgAdmin IngressClass가 traefik으로 고정되지 않았습니다"
if rg -q -F 'kubernetes.io/ingress.class' "$work/pgadmin.yaml"; then
fail "pgAdmin Ingress에 레거시 ingress.class annotation이 남아 있습니다"
fi
rg -q -F 'namespace: platform-data' "$work/pgadmin.yaml" || fail "PostgreSQL 정책 namespace가 다릅니다"
bad_pgadmin_namespaces="$(
awk '
function verify() {
if (pgadmin_instance && namespace != "platform-admin") {
printf "%s/%s=%s\n", kind, name, namespace == "" ? "missing" : namespace
}
}
function reset() {
kind=""
name=""
namespace=""
in_metadata=0
in_labels=0
pgadmin_instance=0
}
/^---$/ {
verify()
reset()
next
}
/^kind:/ {
kind=$2
next
}
/^metadata:$/ {
in_metadata=1
in_labels=0
next
}
in_metadata && /^ name:/ && name == "" {
name=$2
next
}
in_metadata && /^ namespace:/ && namespace == "" {
namespace=$2
next
}
in_metadata && /^ labels:$/ {
in_labels=1
next
}
in_labels && /^ app.kubernetes.io\/instance: pgadmin$/ {
pgadmin_instance=1
next
}
in_metadata && /^[^ ]/ {
in_metadata=0
in_labels=0
}
END {
verify()
}
' "$work/pgadmin.yaml"
)"
[[ -z "$bad_pgadmin_namespaces" ]] || \
fail "pgAdmin instance 리소스 namespace 오류: $bad_pgadmin_namespaces"
rg -q -F 'host: storage-admin.learn.hyeonworks.com' "$work/aistor-admin-oidc.yaml" || fail "AIStor Console Ingress가 없습니다"
rg -q -F 'name: MINIO_IDENTITY_OPENID_CLIENT_SECRET' "$work/aistor-admin-oidc.yaml" || fail "AIStor OIDC Secret 참조가 없습니다"
if rg -q -F 'name: MINIO_IDENTITY_OPENID_VENDOR' "$work/aistor-admin-oidc.yaml"; then
fail "claim-only OIDC profile에 Keycloak Admin API vendor 설정이 있습니다"
fi
if rg -q -F 'host: s3.' "$work/aistor-admin-oidc.yaml"; then
fail "S3 API Ingress가 생성됐습니다"
fi
for name in $NAMES; do
bytes="$(wc -c <"$work/$name.yaml" | tr -d '[:space:]')"
sha="$(sha256sum "$work/$name.yaml" | awk '{print $1}')"
printf '렌더 %-20s %8s bytes SHA-256 %s\n' "$name" "$bytes" "$sha"
if [[ -n "$out" ]]; then
install -m 0600 "$work/$name.yaml" "$out/$name.yaml"
fi
done
if [[ -n "$out" ]]; then
count="$(find "$out" -mindepth 1 -maxdepth 1 -type f | wc -l | tr -d '[:space:]')"
[[ "$count" == 5 ]] || fail "handoff 파일 수가 다릅니다"
fi
printf 'ADMIN SERVICES STATIC RENDER PASS\n'