Files

440 lines
25 KiB
Bash
Executable File

#!/usr/bin/env bash
set -Eeuo pipefail
readonly EXPECTED_KUSTOMIZE_VERSION="v5.8.1"
readonly EXPECTED_HELM_VERSION="v3.19.4"
readonly TARGET_KUBERNETES_VERSION="1.36.2"
readonly AISTOR_OPERATOR_CHART_NAME="aistor-operator"
readonly AISTOR_OPERATOR_CHART_VERSION="5.10.0"
readonly EXPECTED_AISTOR_OPERATOR_SHA256="e5534f5ae4f6f12a3528a8cda5954d73280cba1a8e979e1628fbf3aac76babd1"
readonly AISTOR_OBJECTSTORE_CHART_NAME="aistor-objectstore"
readonly AISTOR_OBJECTSTORE_CHART_VERSION="1.0.16"
readonly EXPECTED_AISTOR_OBJECTSTORE_SHA256="50ffa6a4e014cdc48566b237593baab41d039f103cdc50c1b1ac173b8d8bf71e"
readonly AISTOR_CHART_REPOSITORY="https://helm.min.io/"
readonly REPOSITORY_ROOT="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." && pwd -P)"
readonly -a RENDERED_MANIFEST_NAMES=(
phase2-namespaces
aistor-local-pv
keycloak-operator
platform-postgres-keycloak
keycloak
aistor-operator
minio-aistor
aistor-network-policies
)
fail() {
printf 'ERROR: %s\n' "$*" >&2
exit 1
}
assert_regex_count() {
local file="$1"
local pattern="$2"
local expected="$3"
local description="$4"
local actual
[[ -f "$file" ]] || fail "missing validation input: ${file}"
actual="$(rg --count --no-filename -- "$pattern" "$file" || true)"
actual="${actual:-0}"
[[ "$actual" == "$expected" ]] || \
fail "${description}: expected ${expected}, found ${actual}"
}
usage() {
cat <<'USAGE'
Usage:
bash scripts/validate/render-phase2.sh
bash scripts/validate/render-phase2.sh \
--verified-output-dir /tmp/platform-phase2-apply.XXXXXX
The output option is an internal handoff used only by the AIStor apply script.
The destination must be an existing, empty, non-symlink directory created
below /tmp with the platform-phase2-apply.* prefix.
USAGE
}
verified_output_dir=""
case "$#" in
0)
;;
2)
[[ "$1" == "--verified-output-dir" ]] || {
usage >&2
exit 2
}
verified_output_dir="$2"
;;
*)
usage >&2
exit 2
;;
esac
if [[ -n "$verified_output_dir" ]]; then
[[ "$verified_output_dir" == /tmp/platform-phase2-apply.* ]] || \
fail "verified output directory must match /tmp/platform-phase2-apply.*"
[[ -d "$verified_output_dir" && ! -L "$verified_output_dir" ]] || \
fail "verified output directory must be an existing non-symlink directory"
[[ "$(cd -- "$verified_output_dir" && pwd -P)" == "$verified_output_dir" ]] || \
fail "verified output directory must be an absolute canonical path"
[[ -O "$verified_output_dir" ]] || \
fail "verified output directory must be owned by the current user"
[[ "$(stat --format='%a' -- "$verified_output_dir")" == "700" ]] || \
fail "verified output directory must have mode 0700"
[[ -z "$(find "$verified_output_dir" -mindepth 1 -maxdepth 1 -print -quit)" ]] || \
fail "verified output directory must be empty"
fi
for command_name in cmp find install kubectl rg sha256sum stat tar; do
command -v "$command_name" >/dev/null 2>&1 || \
fail "${command_name} is required"
done
if [[ -n "${PLATFORM_HELM_BIN:-}" ]]; then
[[ "$PLATFORM_HELM_BIN" == /* ]] || \
fail "PLATFORM_HELM_BIN must be an absolute path"
[[ -f "$PLATFORM_HELM_BIN" && -x "$PLATFORM_HELM_BIN" ]] || \
fail "PLATFORM_HELM_BIN is not an executable file: ${PLATFORM_HELM_BIN}"
readonly HELM_BIN="$PLATFORM_HELM_BIN"
else
HELM_BIN="$(command -v helm 2>/dev/null)" || \
fail "Helm ${EXPECTED_HELM_VERSION} is required"
readonly HELM_BIN
fi
kustomize_version="$(kubectl version --client --output=yaml | sed -n 's/^kustomizeVersion: //p')"
helm_version="$("$HELM_BIN" version --template '{{.Version}}')"
[[ "$kustomize_version" == "$EXPECTED_KUSTOMIZE_VERSION" ]] || \
fail "expected Kustomize ${EXPECTED_KUSTOMIZE_VERSION}, found ${kustomize_version:-unknown}"
[[ "$helm_version" == "$EXPECTED_HELM_VERSION" ]] || \
fail "expected Helm ${EXPECTED_HELM_VERSION}, found ${helm_version:-unknown}"
printf 'Validating the Phase 1 baseline and Gitea OIDC desired profile first.\n'
PLATFORM_HELM_BIN="$HELM_BIN" \
bash "${REPOSITORY_ROOT}/scripts/validate/render-phase1.sh"
render_dir="$(mktemp -d "${TMPDIR:-/tmp}/platform-phase2-render.XXXXXX")"
declare -a generated_chart_cache_dirs=()
declare -a generated_chart_cache_parent_dirs=()
cleanup() {
local cache_dir
local parent_dir
for cache_dir in "${generated_chart_cache_dirs[@]}"; do
case "$cache_dir" in
"${REPOSITORY_ROOT}/infrastructure/controllers/aistor-operator/.helm/charts/aistor-operator-5.10.0"|\
"${REPOSITORY_ROOT}/services/minio-aistor/base/deployment/.helm/charts/aistor-objectstore-1.0.16")
rm -rf -- "$cache_dir"
;;
*)
printf 'WARNING: refusing to remove unexpected chart cache: %s\n' \
"$cache_dir" >&2
;;
esac
done
for parent_dir in "${generated_chart_cache_parent_dirs[@]}"; do
case "$parent_dir" in
"${REPOSITORY_ROOT}/infrastructure/controllers/aistor-operator/.helm/charts"|\
"${REPOSITORY_ROOT}/infrastructure/controllers/aistor-operator/.helm"|\
"${REPOSITORY_ROOT}/services/minio-aistor/base/deployment/.helm/charts"|\
"${REPOSITORY_ROOT}/services/minio-aistor/base/deployment/.helm")
rmdir -- "$parent_dir" 2>/dev/null || true
;;
*)
printf 'WARNING: refusing to remove unexpected chart cache parent: %s\n' \
"$parent_dir" >&2
;;
esac
done
case "$render_dir" in
/tmp/platform-phase2-render.*|"${TMPDIR:-/tmp}"/platform-phase2-render.*)
rm -rf -- "$render_dir"
;;
*)
printf 'WARNING: refusing to remove unexpected render directory: %s\n' \
"$render_dir" >&2
;;
esac
}
trap cleanup EXIT
trap 'exit 130' INT
trap 'exit 143' TERM
render_plain() {
local label="$1"
local relative_path="$2"
local output="${render_dir}/${label}.yaml"
kubectl kustomize "${REPOSITORY_ROOT}/${relative_path}" >"$output"
[[ -s "$output" ]] || fail "${label} rendered an empty manifest"
printf 'Rendered %-24s %8s bytes\n' \
"$label" "$(wc -c <"$output" | tr -d '[:space:]')"
}
render_helm() {
local label="$1"
local relative_path="$2"
local output="${render_dir}/${label}.yaml"
kubectl kustomize \
--enable-helm \
--helm-command "$HELM_BIN" \
--helm-kube-version "$TARGET_KUBERNETES_VERSION" \
"${REPOSITORY_ROOT}/${relative_path}" >"$output"
[[ -s "$output" ]] || fail "${label} rendered an empty manifest"
printf 'Rendered %-24s %8s bytes\n' \
"$label" "$(wc -c <"$output" | tr -d '[:space:]')"
}
prepare_verified_chart_cache() {
local label="$1"
local chart_name="$2"
local chart_version="$3"
local build_root_relative_path="$4"
local expected_digest="$5"
local package_path="${render_dir}/${chart_name}-${chart_version}.tgz"
local cache_version_dir="${REPOSITORY_ROOT}/${build_root_relative_path}/.helm/charts/${chart_name}-${chart_version}"
local checksum_output
local actual_digest
local chart_cache_dir="${cache_version_dir%/*}"
local helm_cache_dir="${chart_cache_dir%/*}"
local cache_parent
case "$cache_version_dir" in
"${REPOSITORY_ROOT}/infrastructure/controllers/aistor-operator/.helm/charts/aistor-operator-5.10.0"|\
"${REPOSITORY_ROOT}/services/minio-aistor/base/deployment/.helm/charts/aistor-objectstore-1.0.16")
;;
*)
fail "refusing to create unexpected chart cache: ${cache_version_dir}"
;;
esac
for cache_parent in "$helm_cache_dir" "$chart_cache_dir"; do
[[ ! -L "$cache_parent" ]] || \
fail "refusing symlinked chart cache parent: ${cache_parent}"
[[ ! -e "$cache_parent" || -d "$cache_parent" ]] || \
fail "chart cache parent is not a directory: ${cache_parent}"
done
[[ ! -e "$cache_version_dir" && ! -L "$cache_version_dir" ]] || \
fail "generated chart cache already exists; remove it after confirming it is disposable: ${cache_version_dir}"
"$HELM_BIN" pull "$chart_name" \
--repo "$AISTOR_CHART_REPOSITORY" \
--version "$chart_version" \
--destination "$render_dir"
[[ -f "$package_path" ]] || \
fail "${label} chart package was not downloaded: ${package_path}"
checksum_output="$(sha256sum -- "$package_path")"
actual_digest="${checksum_output%% *}"
[[ "$actual_digest" == "$expected_digest" ]] || \
fail "${label} chart SHA-256 mismatch: expected ${expected_digest}, found ${actual_digest}"
[[ -e "$chart_cache_dir" ]] || generated_chart_cache_parent_dirs+=("$chart_cache_dir")
[[ -e "$helm_cache_dir" ]] || generated_chart_cache_parent_dirs+=("$helm_cache_dir")
mkdir -p -- "$cache_version_dir"
generated_chart_cache_dirs+=("$cache_version_dir")
tar -xzf "$package_path" -C "$cache_version_dir"
[[ -f "${cache_version_dir}/${chart_name}/Chart.yaml" ]] || \
fail "${label} extracted chart is missing Chart.yaml"
printf 'Verified %-24s SHA-256 %s\n' "$label" "$actual_digest"
}
cd -- "$REPOSITORY_ROOT"
if rg --line-number --glob '*.yaml' --glob '*.yml' \
--glob '!**/.helm/**' --glob '!**/charts/**' \
'^[[:space:]]*kind:[[:space:]]*Secret[[:space:]]*$' \
infrastructure services bootstrap clusters components; then
fail "a source-controlled Kubernetes Secret manifest was found"
fi
if rg --line-number --glob 'kustomization.yaml' \
'LoadRestrictionsNone|load-restrictor' .; then
fail "the repository must keep Kustomize LoadRestrictionsRootOnly"
fi
keycloak_operator_kustomization="${REPOSITORY_ROOT}/infrastructure/controllers/keycloak-operator/kustomization.yaml"
aistor_operator_kustomization="${REPOSITORY_ROOT}/infrastructure/controllers/aistor-operator/kustomization.yaml"
aistor_objectstore_kustomization="${REPOSITORY_ROOT}/services/minio-aistor/base/deployment/kustomization.yaml"
aistor_operator_values="${REPOSITORY_ROOT}/infrastructure/controllers/aistor-operator/values/home.yaml"
aistor_objectstore_values="${REPOSITORY_ROOT}/services/minio-aistor/base/deployment/values/home.yaml"
assert_regex_count "$keycloak_operator_kustomization" '^[[:space:]]*-[[:space:]]github\.com/keycloak/keycloak-k8s-resources/kubernetes\?ref=26\.7\.0[[:space:]]*$' 1 "Keycloak Operator remote ref must be 26.7.0"
assert_regex_count "$keycloak_operator_kustomization" 'github\.com/keycloak/keycloak-k8s-resources/kubernetes' 1 "exactly one Keycloak Operator remote resource is allowed"
assert_regex_count "$keycloak_operator_kustomization" '\?ref=' 1 "exactly one pinned remote ref is allowed in the Keycloak Operator root"
assert_regex_count "$aistor_operator_kustomization" '^[[:space:]]*-[[:space:]]name:' 1 "AIStor Operator root must contain one Helm chart"
assert_regex_count "$aistor_operator_kustomization" '^[[:space:]]*-[[:space:]]name:[[:space:]]aistor-operator[[:space:]]*$' 1 "AIStor Operator chart name is pinned"
assert_regex_count "$aistor_operator_kustomization" '^[[:space:]]*repo:[[:space:]]https://helm\.min\.io/[[:space:]]*$' 1 "AIStor Operator chart repository is pinned"
assert_regex_count "$aistor_operator_kustomization" '^[[:space:]]*version:[[:space:]]5\.10\.0[[:space:]]*$' 1 "AIStor Operator chart version is pinned"
assert_regex_count "$aistor_operator_kustomization" '^[[:space:]]*valuesFile:[[:space:]]values/home\.yaml[[:space:]]*$' 1 "AIStor Operator values file is pinned"
assert_regex_count "$aistor_operator_kustomization" '^[[:space:]]*includeCRDs:[[:space:]]true[[:space:]]*$' 1 "AIStor Operator CRDs must be included"
assert_regex_count "$aistor_operator_kustomization" '^[[:space:]]*chartHome:[[:space:]]\.helm/charts[[:space:]]*$' 1 "AIStor Operator chartHome is pinned"
assert_regex_count "$aistor_objectstore_kustomization" '^[[:space:]]*-[[:space:]]name:' 1 "AIStor ObjectStore root must contain one Helm chart"
assert_regex_count "$aistor_objectstore_kustomization" '^[[:space:]]*-[[:space:]]name:[[:space:]]aistor-objectstore[[:space:]]*$' 1 "AIStor ObjectStore chart name is pinned"
assert_regex_count "$aistor_objectstore_kustomization" '^[[:space:]]*repo:[[:space:]]https://helm\.min\.io/[[:space:]]*$' 1 "AIStor ObjectStore chart repository is pinned"
assert_regex_count "$aistor_objectstore_kustomization" '^[[:space:]]*version:[[:space:]]1\.0\.16[[:space:]]*$' 1 "AIStor ObjectStore chart version is pinned"
assert_regex_count "$aistor_objectstore_kustomization" '^[[:space:]]*valuesFile:[[:space:]]values/home\.yaml[[:space:]]*$' 1 "AIStor ObjectStore values file is pinned"
assert_regex_count "$aistor_objectstore_kustomization" '^[[:space:]]*includeCRDs:[[:space:]]false[[:space:]]*$' 1 "AIStor ObjectStore root must not duplicate CRDs"
assert_regex_count "$aistor_objectstore_kustomization" '^[[:space:]]*chartHome:[[:space:]]\.helm/charts[[:space:]]*$' 1 "AIStor ObjectStore chartHome is pinned"
assert_regex_count "$aistor_operator_values" '^license:[[:space:]]*""[[:space:]]*$' 1 "AIStor license payload must not be rendered from values"
assert_regex_count "$aistor_objectstore_values" '^[[:space:]]*existingSecret:[[:space:]]true[[:space:]]*$' 1 "AIStor ObjectStore must use an existing Secret"
assert_regex_count "$aistor_objectstore_values" '^[[:space:]]*name:[[:space:]]aistor-root-configuration[[:space:]]*$' 2 "AIStor configuration Secret references are pinned"
assert_regex_count "$aistor_objectstore_values" '^[[:space:]]*serviceType:[[:space:]]ClusterIP[[:space:]]*$' 2 "AIStor S3 and Console services must be ClusterIP"
render_plain phase2-namespaces infrastructure/namespaces/phase2
render_plain aistor-local-pv infrastructure/storage/aistor-local-pv
render_plain aistor-network-policies infrastructure/networking/aistor
render_plain keycloak-operator infrastructure/controllers/keycloak-operator
render_plain platform-postgres-keycloak services/platform-postgres-keycloak
render_plain keycloak services/keycloak
prepare_verified_chart_cache \
aistor-operator-chart \
"$AISTOR_OPERATOR_CHART_NAME" \
"$AISTOR_OPERATOR_CHART_VERSION" \
infrastructure/controllers/aistor-operator \
"$EXPECTED_AISTOR_OPERATOR_SHA256"
render_helm aistor-operator infrastructure/controllers/aistor-operator
prepare_verified_chart_cache \
aistor-objectstore-chart \
"$AISTOR_OBJECTSTORE_CHART_NAME" \
"$AISTOR_OBJECTSTORE_CHART_VERSION" \
services/minio-aistor/base/deployment \
"$EXPECTED_AISTOR_OBJECTSTORE_SHA256"
render_helm minio-aistor services/minio-aistor
if rg --line-number '^[[:space:]]*kind:[[:space:]]*Secret[[:space:]]*$' \
"${render_dir}/phase2-namespaces.yaml" \
"${render_dir}/aistor-local-pv.yaml" \
"${render_dir}/keycloak-operator.yaml" \
"${render_dir}/platform-postgres-keycloak.yaml" \
"${render_dir}/keycloak.yaml" \
"${render_dir}/aistor-operator.yaml" \
"${render_dir}/minio-aistor.yaml" \
"${render_dir}/aistor-network-policies.yaml"; then
fail "a Phase 2 render unexpectedly contains a Secret"
fi
assert_regex_count "${render_dir}/phase2-namespaces.yaml" '^kind: Namespace$' 3 "Phase 2 namespace count"
assert_regex_count "${render_dir}/aistor-local-pv.yaml" '^kind: StorageClass$' 1 "AIStor StorageClass count"
assert_regex_count "${render_dir}/aistor-local-pv.yaml" '^kind: PersistentVolume$' 1 "AIStor PersistentVolume count"
assert_regex_count "${render_dir}/aistor-network-policies.yaml" '^kind: NetworkPolicy$' 11 "AIStor NetworkPolicy count"
assert_regex_count "${render_dir}/platform-postgres-keycloak.yaml" '^kind: DatabaseRole$' 1 "Keycloak DatabaseRole count"
assert_regex_count "${render_dir}/platform-postgres-keycloak.yaml" '^kind: Database$' 1 "Keycloak Database count"
assert_regex_count "${render_dir}/platform-postgres-keycloak.yaml" '^kind: NetworkPolicy$' 1 "Keycloak PostgreSQL NetworkPolicy count"
assert_regex_count "${render_dir}/platform-postgres-keycloak.yaml" '^kind: Cluster$' 0 "the Phase 2 PostgreSQL extension must not contain a Cluster"
assert_regex_count "${render_dir}/keycloak.yaml" '^kind: Keycloak$' 1 "Keycloak custom resource count"
assert_regex_count "${render_dir}/keycloak.yaml" '^kind: Ingress$' 1 "Keycloak Ingress count"
assert_regex_count "${render_dir}/minio-aistor.yaml" '^kind: ObjectStore$' 1 "AIStor ObjectStore count"
assert_regex_count "${render_dir}/keycloak.yaml" '^[[:space:]]*instances:[[:space:]]1[[:space:]]*$' 1 "Keycloak instance count field"
assert_regex_count "${render_dir}/keycloak.yaml" '^[[:space:]]*hostname:[[:space:]]https://id\.learn\.hyeonworks\.com[[:space:]]*$' 1 "Keycloak external hostname"
assert_regex_count "${render_dir}/keycloak.yaml" '^[[:space:]]*strict:[[:space:]]true[[:space:]]*$' 1 "Keycloak strict hostname mode"
assert_regex_count "${render_dir}/keycloak.yaml" '^[[:space:]]*httpEnabled:[[:space:]]true[[:space:]]*$' 1 "Keycloak internal HTTP mode"
assert_regex_count "${render_dir}/keycloak.yaml" '^[[:space:]]*httpPort:[[:space:]]8080[[:space:]]*$' 1 "Keycloak HTTP port"
assert_regex_count "${render_dir}/keycloak.yaml" '^[[:space:]]*serviceHttpPort:[[:space:]]8080[[:space:]]*$' 1 "Keycloak Service HTTP port"
assert_regex_count "${render_dir}/keycloak.yaml" '^[[:space:]]*headers:[[:space:]]xforwarded[[:space:]]*$' 1 "Keycloak forwarded-header mode"
assert_regex_count "${render_dir}/keycloak.yaml" '^[[:space:]]*enabled:[[:space:]]false[[:space:]]*$' 1 "Keycloak Operator-managed Ingress must be disabled"
assert_regex_count "${render_dir}/keycloak.yaml" '^[[:space:]]*ingressClassName:[[:space:]]traefik[[:space:]]*$' 1 "Keycloak Ingress class"
assert_regex_count "${render_dir}/keycloak.yaml" '^[[:space:]]*-[[:space:]]host:[[:space:]]id\.learn\.hyeonworks\.com[[:space:]]*$' 1 "Keycloak Ingress host"
assert_regex_count "${render_dir}/keycloak.yaml" '^[[:space:]]*name:[[:space:]]keycloak-service[[:space:]]*$' 1 "Keycloak Ingress backend Service"
assert_regex_count "${render_dir}/keycloak.yaml" '^[[:space:]]*number:[[:space:]]8080[[:space:]]*$' 1 "Keycloak Ingress backend port"
assert_regex_count "${render_dir}/keycloak.yaml" '^[[:space:]]*kubernetes\.io/metadata\.name:[[:space:]]kube-system[[:space:]]*$' 2 "Keycloak Traefik namespace selectors"
assert_regex_count "${render_dir}/keycloak.yaml" '^[[:space:]]*app\.kubernetes\.io/name:[[:space:]]traefik[[:space:]]*$' 2 "Keycloak Traefik pod selectors"
assert_regex_count "${render_dir}/keycloak.yaml" '^[[:space:]]*kubernetes\.io/metadata\.name:[[:space:]]keycloak[[:space:]]*$' 1 "Keycloak management namespace selector"
assert_regex_count "${render_dir}/keycloak.yaml" '^[[:space:]]*app\.kubernetes\.io/name:[[:space:]]keycloak-operator[[:space:]]*$' 1 "Keycloak management Operator selector"
assert_regex_count "${render_dir}/keycloak.yaml" '^[[:space:]]*xaEnabled:[[:space:]]false[[:space:]]*$' 1 "Keycloak XA transaction mode"
assert_regex_count "${render_dir}/keycloak.yaml" ':[[:space:]]*9000[[:space:]]*$' 0 "Keycloak management port must not be exposed"
assert_regex_count "${render_dir}/platform-postgres-keycloak.yaml" '^[[:space:]]*name:[[:space:]]platform-postgres-keycloak[[:space:]]*$' 2 "Keycloak database and role names"
assert_regex_count "${render_dir}/platform-postgres-keycloak.yaml" '^[[:space:]]*name:[[:space:]]keycloak-db-credentials[[:space:]]*$' 1 "Keycloak DatabaseRole Secret reference"
assert_regex_count "${render_dir}/platform-postgres-keycloak.yaml" '^[[:space:]]*kubernetes\.io/metadata\.name:[[:space:]]keycloak[[:space:]]*$' 1 "Keycloak PostgreSQL namespace selector"
assert_regex_count "${render_dir}/platform-postgres-keycloak.yaml" '^[[:space:]]*-[[:space:]]port:[[:space:]]5432[[:space:]]*$' 1 "Keycloak PostgreSQL ingress port"
assert_regex_count "${render_dir}/platform-postgres-keycloak.yaml" '^[[:space:]]*cnpg\.io/cluster:[[:space:]]platform-postgres[[:space:]]*$' 1 "Keycloak PostgreSQL pod selector"
assert_regex_count "${render_dir}/aistor-local-pv.yaml" '^provisioner:[[:space:]]kubernetes\.io/no-provisioner[[:space:]]*$' 1 "AIStor static StorageClass provisioner"
assert_regex_count "${render_dir}/aistor-local-pv.yaml" '^reclaimPolicy:[[:space:]]Retain[[:space:]]*$' 1 "AIStor StorageClass reclaim policy"
assert_regex_count "${render_dir}/aistor-local-pv.yaml" '^volumeBindingMode:[[:space:]]WaitForFirstConsumer[[:space:]]*$' 1 "AIStor volume binding mode"
assert_regex_count "${render_dir}/aistor-local-pv.yaml" '^[[:space:]]*persistentVolumeReclaimPolicy:[[:space:]]Retain[[:space:]]*$' 1 "AIStor PersistentVolume reclaim policy"
assert_regex_count "${render_dir}/aistor-local-pv.yaml" '^[[:space:]]*storage:[[:space:]]900Gi[[:space:]]*$' 1 "AIStor Local PV capacity"
assert_regex_count "${render_dir}/aistor-local-pv.yaml" '^[[:space:]]*path:[[:space:]]/srv/k3s/aistor[[:space:]]*$' 1 "AIStor Local PV host path"
assert_regex_count "${render_dir}/aistor-local-pv.yaml" '^[[:space:]]*-[[:space:]]donghyeon-system-product-name[[:space:]]*$' 1 "AIStor Local PV node affinity"
assert_regex_count "${render_dir}/aistor-local-pv.yaml" '^[[:space:]]*platform\.hyeonworks\.com/filesystem:[[:space:]]xfs[[:space:]]*$' 1 "AIStor Local PV filesystem label"
assert_regex_count "${render_dir}/aistor-local-pv.yaml" '^[[:space:]]*storageClassName:[[:space:]]aistor-local-xfs-retain[[:space:]]*$' 1 "AIStor Local PV StorageClass binding"
assert_regex_count "${render_dir}/aistor-local-pv.yaml" '^[[:space:]]*-[[:space:]]ReadWriteOnce[[:space:]]*$' 1 "AIStor Local PV access mode"
assert_regex_count "${render_dir}/aistor-operator.yaml" '^kind: CustomResourceDefinition$' 3 "AIStor Operator CRD count"
assert_regex_count "${render_dir}/aistor-operator.yaml" '^kind: Deployment$' 3 "AIStor Operator Deployment count"
assert_regex_count "${render_dir}/aistor-operator.yaml" '^[[:space:]]*caBundle:' 0 "AIStor webhook runtime CA must not be declaratively owned"
assert_regex_count "${render_dir}/aistor-network-policies.yaml" '^[[:space:]]*namespace:[[:space:]]aistor$' 5 "AIStor control-plane NetworkPolicy namespace count"
assert_regex_count "${render_dir}/aistor-network-policies.yaml" '^[[:space:]]*namespace:[[:space:]]object-storage$' 6 "AIStor data-plane NetworkPolicy namespace count"
assert_regex_count "${render_dir}/aistor-network-policies.yaml" '^[[:space:]]*-[[:space:]]port:[[:space:]]8443$' 1 "AIStor admission webhook policy port"
assert_regex_count "${render_dir}/aistor-network-policies.yaml" '^[[:space:]]*-[[:space:]]port:[[:space:]]4221$' 2 "AIStor upgrade service policy port"
assert_regex_count "${render_dir}/minio-aistor.yaml" '^[[:space:]]*servers:[[:space:]]1[[:space:]]*$' 1 "AIStor server count"
assert_regex_count "${render_dir}/minio-aistor.yaml" '^[[:space:]]*volumesPerServer:[[:space:]]1[[:space:]]*$' 1 "AIStor volume count"
assert_regex_count "${render_dir}/minio-aistor.yaml" '^[[:space:]]*storage:[[:space:]]900Gi[[:space:]]*$' 1 "AIStor claim size"
assert_regex_count "${render_dir}/minio-aistor.yaml" '^[[:space:]]*storageClassName:[[:space:]]aistor-local-xfs-retain[[:space:]]*$' 1 "AIStor claim StorageClass"
assert_regex_count "${render_dir}/minio-aistor.yaml" '^[[:space:]]*name:[[:space:]]aistor-root-configuration[[:space:]]*$' 1 "AIStor configuration Secret reference"
assert_regex_count "${render_dir}/minio-aistor.yaml" '^[[:space:]]*pvcProtection:[[:space:]]true[[:space:]]*$' 1 "AIStor PVC protection"
assert_regex_count "${render_dir}/minio-aistor.yaml" '^[[:space:]]*serviceType:[[:space:]]ClusterIP[[:space:]]*$' 2 "AIStor S3 and Console Service types"
assert_regex_count "${render_dir}/minio-aistor.yaml" '^[[:space:]]*disableAutoCert:[[:space:]]true[[:space:]]*$' 1 "AIStor in-cluster TLS mode"
assert_regex_count "${render_dir}/minio-aistor.yaml" '^[[:space:]]*kubernetes\.io/hostname:[[:space:]]donghyeon-system-product-name[[:space:]]*$' 1 "AIStor ObjectStore node selector"
if rg --quiet '^[[:space:]]*kind:[[:space:]]*Ingress[[:space:]]*$|^[[:space:]]*(serviceType|type):[[:space:]]*(NodePort|LoadBalancer)[[:space:]]*$' \
"${render_dir}/minio-aistor.yaml"; then
fail "AIStor must not render Ingress, NodePort, or LoadBalancer exposure"
fi
if rg --quiet '^[[:space:]]*tls:[[:space:]]*(\[\])?[[:space:]]*$' \
"${render_dir}/keycloak.yaml"; then
fail "Keycloak must not render in-cluster TLS"
fi
if rg --quiet '^[[:space:]]*(serviceType|type):[[:space:]]*(NodePort|LoadBalancer)[[:space:]]*$' \
"${render_dir}/keycloak.yaml"; then
fail "Keycloak must not render NodePort or LoadBalancer exposure"
fi
if rg --quiet 'cidr:[[:space:]]*0\.0\.0\.0/0' \
"${render_dir}/aistor-network-policies.yaml"; then
fail "AIStor NetworkPolicies must not allow unrestricted Internet egress"
fi
if [[ -n "$verified_output_dir" ]]; then
for manifest_name in "${RENDERED_MANIFEST_NAMES[@]}"; do
source_manifest="${render_dir}/${manifest_name}.yaml"
output_manifest="${verified_output_dir}/${manifest_name}.yaml"
[[ -f "$source_manifest" && ! -L "$source_manifest" && -s "$source_manifest" ]] || \
fail "validated manifest is missing or unsafe: ${source_manifest}"
install -m 0600 -- "$source_manifest" "$output_manifest"
[[ -f "$output_manifest" && ! -L "$output_manifest" && -s "$output_manifest" ]] || \
fail "verified manifest handoff failed: ${output_manifest}"
cmp --silent -- "$source_manifest" "$output_manifest" || \
fail "verified manifest changed during handoff: ${manifest_name}.yaml"
done
verified_entry_count="$(
find "$verified_output_dir" -mindepth 1 -maxdepth 1 |
wc -l | tr -d '[:space:]'
)"
[[ "$verified_entry_count" == "${#RENDERED_MANIFEST_NAMES[@]}" ]] || \
fail "verified output directory does not contain exactly eight manifest files"
printf 'Preserved eight verified manifests for the AIStor apply handoff.\n'
fi
printf 'Phase 2 rendering and source invariants passed.\n'
printf 'No live ObjectStore CRD or Kubernetes cluster access was required.\n'
printf 'Temporary Phase 2 rendered manifests and generated chart caches will be removed on exit.\n'