440 lines
25 KiB
Bash
Executable File
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'
|