#!/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'