244 lines
8.7 KiB
Bash
Executable File
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'
|