672 lines
27 KiB
Bash
Executable File
672 lines
27 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 CNPG_CHART_NAME="cloudnative-pg"
|
|
readonly CNPG_CHART_VERSION="0.29.0"
|
|
readonly CNPG_CHART_REPOSITORY="https://cloudnative-pg.github.io/charts"
|
|
readonly EXPECTED_CNPG_CHART_SHA256="668e065ff53508d58238788fd35b355a925060843629a951df0e6a9362e6d32f"
|
|
readonly GITEA_CHART_NAME="gitea"
|
|
readonly GITEA_CHART_VERSION="12.7.0"
|
|
readonly GITEA_CHART_REPOSITORY="https://dl.gitea.com/charts/"
|
|
readonly EXPECTED_GITEA_CHART_SHA256="5881ef9c59400bee2d5547e77c4cd0efb925143c2f5d93fb4f38446db76b0167"
|
|
readonly REPOSITORY_ROOT="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." && pwd -P)"
|
|
readonly -a RENDERED_MANIFEST_NAMES=(
|
|
namespaces
|
|
ssd-local-pv
|
|
cnpg-operator
|
|
platform-postgres
|
|
gitea
|
|
gitea-oidc
|
|
)
|
|
|
|
fail() {
|
|
printf 'ERROR: %s\n' "$*" >&2
|
|
exit 1
|
|
}
|
|
|
|
assert_exact_source_text() {
|
|
local file="$1"
|
|
local expected_text="$2"
|
|
local description="$3"
|
|
local count
|
|
|
|
count="$(rg --count-matches --fixed-strings -- "$expected_text" "$file" || true)"
|
|
[[ "$count" == "1" ]] || \
|
|
fail "${description} must appear exactly once in ${file#${REPOSITORY_ROOT}/}"
|
|
}
|
|
|
|
usage() {
|
|
cat <<'USAGE'
|
|
Usage:
|
|
bash scripts/validate/render-phase1.sh
|
|
bash scripts/validate/render-phase1.sh \
|
|
--verified-output-dir /tmp/platform-phase1-apply.XXXXXX
|
|
|
|
The output option is an internal handoff used only by the Phase 1 apply
|
|
script. The destination must be an existing, empty, non-symlink directory
|
|
created directly below /tmp with the platform-phase1-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-phase1-apply.* ]] || \
|
|
fail "verified output directory must match /tmp/platform-phase1-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
|
|
|
|
command -v kubectl >/dev/null 2>&1 || fail "kubectl is required"
|
|
command -v jq >/dev/null 2>&1 || fail "jq is required"
|
|
command -v rg >/dev/null 2>&1 || fail "ripgrep (rg) is required"
|
|
command -v sha256sum >/dev/null 2>&1 || fail "sha256sum is required"
|
|
command -v tar >/dev/null 2>&1 || fail "tar is required"
|
|
command -v cmp >/dev/null 2>&1 || fail "cmp is required"
|
|
command -v find >/dev/null 2>&1 || fail "find is required"
|
|
command -v install >/dev/null 2>&1 || fail "install is required"
|
|
command -v stat >/dev/null 2>&1 || fail "stat is required"
|
|
|
|
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}"
|
|
|
|
render_dir="$(mktemp -d "${TMPDIR:-/tmp}/platform-phase1-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/cloudnative-pg/.helm/charts/cloudnative-pg-0.29.0"|\
|
|
"${REPOSITORY_ROOT}/services/gitea/profiles/oidc/.helm/charts/gitea-12.7.0")
|
|
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/cloudnative-pg/.helm/charts"|\
|
|
"${REPOSITORY_ROOT}/infrastructure/controllers/cloudnative-pg/.helm"|\
|
|
"${REPOSITORY_ROOT}/services/gitea/profiles/oidc/.helm/charts"|\
|
|
"${REPOSITORY_ROOT}/services/gitea/profiles/oidc/.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-phase1-render.*|"${TMPDIR:-/tmp}"/platform-phase1-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 %-20s %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 %-20s %8s bytes\n' "$label" "$(wc -c <"$output" | tr -d '[:space:]')"
|
|
}
|
|
|
|
extract_rendered_document() {
|
|
local manifest="$1"
|
|
local wanted_kind="$2"
|
|
local wanted_name="$3"
|
|
local output="$4"
|
|
|
|
awk \
|
|
-v wanted_kind="$wanted_kind" \
|
|
-v wanted_name="$wanted_name" \
|
|
'
|
|
function reset_document() {
|
|
document = ""
|
|
document_kind = ""
|
|
document_name = ""
|
|
in_metadata = 0
|
|
}
|
|
|
|
function flush_document() {
|
|
if (document_kind == wanted_kind && document_name == wanted_name) {
|
|
matches++
|
|
printf "%s", document
|
|
}
|
|
}
|
|
|
|
BEGIN {
|
|
reset_document()
|
|
}
|
|
|
|
/^---[[:space:]]*$/ {
|
|
flush_document()
|
|
reset_document()
|
|
next
|
|
}
|
|
|
|
{
|
|
document = document $0 ORS
|
|
|
|
if ($0 ~ /^kind:[[:space:]]*/) {
|
|
document_kind = $0
|
|
sub(/^kind:[[:space:]]*/, "", document_kind)
|
|
sub(/[[:space:]]*$/, "", document_kind)
|
|
}
|
|
|
|
if ($0 == "metadata:") {
|
|
in_metadata = 1
|
|
next
|
|
}
|
|
|
|
if (in_metadata && $0 ~ /^ name:[[:space:]]*/) {
|
|
document_name = $0
|
|
sub(/^ name:[[:space:]]*/, "", document_name)
|
|
sub(/[[:space:]]*$/, "", document_name)
|
|
in_metadata = 0
|
|
} else if (in_metadata && $0 ~ /^[^[:space:]]/) {
|
|
in_metadata = 0
|
|
}
|
|
}
|
|
|
|
END {
|
|
flush_document()
|
|
if (matches != 1) {
|
|
exit 42
|
|
}
|
|
}
|
|
' \
|
|
"$manifest" >"$output"
|
|
}
|
|
|
|
prepare_verified_chart_cache() {
|
|
local label="$1"
|
|
local chart_name="$2"
|
|
local chart_repository="$3"
|
|
local chart_version="$4"
|
|
local build_root_relative_path="$5"
|
|
local expected_digest="$6"
|
|
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
|
|
|
|
if [[ ! -f "$package_path" ]]; then
|
|
"$HELM_BIN" pull "$chart_name" \
|
|
--repo "$chart_repository" \
|
|
--version "$chart_version" \
|
|
--destination "$render_dir"
|
|
fi
|
|
|
|
[[ -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 package SHA-256 mismatch: expected ${expected_digest}, found ${actual_digest}"
|
|
|
|
case "$cache_version_dir" in
|
|
"${REPOSITORY_ROOT}/infrastructure/controllers/cloudnative-pg/.helm/charts/cloudnative-pg-0.29.0"|\
|
|
"${REPOSITORY_ROOT}/services/gitea/profiles/oidc/.helm/charts/gitea-12.7.0")
|
|
;;
|
|
*)
|
|
fail "refusing to create unexpected chart cache directory: ${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 only after confirming it is disposable: ${cache_version_dir}"
|
|
[[ -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 %-20s 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
|
|
|
|
readonly GITEA_BASELINE_KUSTOMIZATION="${REPOSITORY_ROOT}/services/gitea/kustomization.yaml"
|
|
readonly GITEA_OIDC_KUSTOMIZATION="${REPOSITORY_ROOT}/services/gitea/profiles/oidc/kustomization.yaml"
|
|
for gitea_kustomization in \
|
|
"$GITEA_BASELINE_KUSTOMIZATION" \
|
|
"$GITEA_OIDC_KUSTOMIZATION"; do
|
|
assert_exact_source_text "$gitea_kustomization" \
|
|
'repo: https://dl.gitea.com/charts/' \
|
|
"the pinned Gitea Chart repository"
|
|
assert_exact_source_text "$gitea_kustomization" \
|
|
'version: 12.7.0' \
|
|
"the pinned Gitea Chart version"
|
|
assert_exact_source_text "$gitea_kustomization" \
|
|
'includeCRDs: false' \
|
|
"the Gitea includeCRDs policy"
|
|
assert_exact_source_text "$gitea_kustomization" \
|
|
'skipTests: true' \
|
|
"the Gitea Helm test policy"
|
|
done
|
|
assert_exact_source_text "$GITEA_BASELINE_KUSTOMIZATION" \
|
|
'chartHome: profiles/oidc/.helm/charts' \
|
|
"the baseline Gitea verified Chart cache"
|
|
assert_exact_source_text "$GITEA_BASELINE_KUSTOMIZATION" \
|
|
'valuesFile: profiles/oidc/values/baseline.yaml' \
|
|
"the baseline Gitea values path"
|
|
assert_exact_source_text "$GITEA_OIDC_KUSTOMIZATION" \
|
|
'chartHome: .helm/charts' \
|
|
"the OIDC Gitea verified Chart cache"
|
|
assert_exact_source_text "$GITEA_OIDC_KUSTOMIZATION" \
|
|
'valuesFile: values/baseline.yaml' \
|
|
"the OIDC Gitea baseline values path"
|
|
assert_exact_source_text "$GITEA_OIDC_KUSTOMIZATION" \
|
|
'additionalValuesFiles:' \
|
|
"the OIDC Gitea values merge"
|
|
assert_exact_source_text "$GITEA_OIDC_KUSTOMIZATION" \
|
|
'values/oidc.yaml' \
|
|
"the OIDC Gitea override path"
|
|
|
|
render_plain namespaces infrastructure/namespaces/overlays/home
|
|
render_plain ssd-local-pv infrastructure/storage/ssd-local-pv
|
|
prepare_verified_chart_cache \
|
|
cloudnative-pg-chart \
|
|
"$CNPG_CHART_NAME" \
|
|
"$CNPG_CHART_REPOSITORY" \
|
|
"$CNPG_CHART_VERSION" \
|
|
infrastructure/controllers/cloudnative-pg \
|
|
"$EXPECTED_CNPG_CHART_SHA256"
|
|
render_helm cnpg-operator infrastructure/controllers/cloudnative-pg
|
|
render_plain platform-postgres services/platform-postgres
|
|
prepare_verified_chart_cache \
|
|
gitea-chart \
|
|
"$GITEA_CHART_NAME" \
|
|
"$GITEA_CHART_REPOSITORY" \
|
|
"$GITEA_CHART_VERSION" \
|
|
services/gitea/profiles/oidc \
|
|
"$EXPECTED_GITEA_CHART_SHA256"
|
|
render_helm gitea services/gitea
|
|
|
|
render_helm gitea-oidc services/gitea/profiles/oidc
|
|
[[ "$(awk '$0 == "kind: Cluster" { count++ } END { print count + 0 }' "${render_dir}/platform-postgres.yaml")" == "1" ]] || \
|
|
fail "the Phase 1 PostgreSQL root must contain exactly one Cluster"
|
|
[[ "$(awk '$0 == "kind: DatabaseRole" { count++ } END { print count + 0 }' "${render_dir}/platform-postgres.yaml")" == "1" ]] || \
|
|
fail "the Phase 1 PostgreSQL root must contain exactly one Gitea DatabaseRole"
|
|
[[ "$(awk '$0 == "kind: Database" { count++ } END { print count + 0 }' "${render_dir}/platform-postgres.yaml")" == "1" ]] || \
|
|
fail "the Phase 1 PostgreSQL root must contain exactly one Gitea Database"
|
|
[[ "$(awk '$0 == "kind: NetworkPolicy" { count++ } END { print count + 0 }' "${render_dir}/platform-postgres.yaml")" == "1" ]] || \
|
|
fail "the Phase 1 PostgreSQL root must contain exactly one base NetworkPolicy"
|
|
[[ "$(rg --count -- '^[[:space:]]*name:[[:space:]]platform-postgres-gitea[[:space:]]*$' "${render_dir}/platform-postgres.yaml" || true)" == "2" ]] || \
|
|
fail "the Phase 1 PostgreSQL root must contain the Gitea DatabaseRole and Database"
|
|
if rg --quiet '^[[:space:]]*name:[[:space:]]platform-postgres-keycloak[[:space:]]*$' "${render_dir}/platform-postgres.yaml"; then
|
|
fail "the Phase 1 PostgreSQL root must not contain a Keycloak DatabaseRole or Database"
|
|
fi
|
|
[[ "$(rg --count -- '^[[:space:]]*-[[:space:]]host gitea gitea all scram-sha-256[[:space:]]*$' "${render_dir}/platform-postgres.yaml" || true)" == "1" ]] || \
|
|
fail "the Gitea role must authenticate only to the Gitea database"
|
|
[[ "$(rg --count -- '^[[:space:]]*-[[:space:]]host all gitea all reject[[:space:]]*$' "${render_dir}/platform-postgres.yaml" || true)" == "1" ]] || \
|
|
fail "the Gitea role must be rejected from every other database"
|
|
[[ "$(rg --count -- '^[[:space:]]*-[[:space:]]host keycloak keycloak all scram-sha-256[[:space:]]*$' "${render_dir}/platform-postgres.yaml" || true)" == "1" ]] || \
|
|
fail "the Keycloak role must authenticate only to the Keycloak database"
|
|
[[ "$(rg --count -- '^[[:space:]]*-[[:space:]]host all keycloak all reject[[:space:]]*$' "${render_dir}/platform-postgres.yaml" || true)" == "1" ]] || \
|
|
fail "the Keycloak role must be rejected from every other database"
|
|
|
|
|
|
rg --quiet '^kind: CustomResourceDefinition$' "${render_dir}/cnpg-operator.yaml" || \
|
|
fail "CloudNativePG CRDs are missing from the operator render"
|
|
rg --quiet '^kind: Cluster$' "${render_dir}/platform-postgres.yaml" || \
|
|
fail "the platform PostgreSQL Cluster is missing"
|
|
rg --quiet '^kind: DatabaseRole$' "${render_dir}/platform-postgres.yaml" || \
|
|
fail "the Gitea DatabaseRole is missing"
|
|
rg --quiet '^kind: Database$' "${render_dir}/platform-postgres.yaml" || \
|
|
fail "the Gitea Database is missing"
|
|
for gitea_profile in gitea gitea-oidc; do
|
|
gitea_profile_manifest="${render_dir}/${gitea_profile}.yaml"
|
|
gitea_profile_deployment_document="${render_dir}/${gitea_profile}-deployment.yaml"
|
|
gitea_profile_ingress_document="${render_dir}/${gitea_profile}-ingress.yaml"
|
|
gitea_profile_servicemonitor_document="${render_dir}/${gitea_profile}-servicemonitor.yaml"
|
|
|
|
rg --quiet '^kind: PersistentVolumeClaim$' "$gitea_profile_manifest" || \
|
|
fail "${gitea_profile} is missing the Gitea PVC"
|
|
rg --quiet '^kind: Ingress$' "$gitea_profile_manifest" || \
|
|
fail "${gitea_profile} is missing the Gitea Ingress"
|
|
rg --quiet \
|
|
'^[[:space:]]*-[[:space:]]*host:[[:space:]]*git\.learn\.hyeonworks\.com[[:space:]]*$' \
|
|
"$gitea_profile_manifest" || \
|
|
fail "${gitea_profile} Ingress host is not git.learn.hyeonworks.com"
|
|
[[ "$(rg --count-matches \
|
|
'^[[:space:]]*ROOT_URL=https://git\.learn\.hyeonworks\.com/[[:space:]]*$' \
|
|
"$gitea_profile_manifest" || true)" == "1" ]] || \
|
|
fail "${gitea_profile} must render the external HTTPS ROOT_URL exactly once"
|
|
|
|
extract_rendered_document \
|
|
"$gitea_profile_manifest" \
|
|
Deployment \
|
|
gitea \
|
|
"$gitea_profile_deployment_document" || \
|
|
fail "${gitea_profile} must contain exactly one gitea Deployment"
|
|
extract_rendered_document \
|
|
"$gitea_profile_manifest" \
|
|
Ingress \
|
|
gitea-http \
|
|
"$gitea_profile_ingress_document" || \
|
|
fail "${gitea_profile} must contain exactly one gitea-http Ingress"
|
|
extract_rendered_document \
|
|
"$gitea_profile_manifest" \
|
|
ServiceMonitor \
|
|
gitea \
|
|
"$gitea_profile_servicemonitor_document" || \
|
|
fail "${gitea_profile} must contain exactly one gitea ServiceMonitor"
|
|
if rg --quiet '^[[:space:]]{2}tls:[[:space:]]*' "$gitea_profile_ingress_document"; then
|
|
fail "${gitea_profile} must not render an in-cluster TLS section"
|
|
fi
|
|
|
|
if ! kubectl create --dry-run=client \
|
|
-f "$gitea_profile_servicemonitor_document" \
|
|
-o json | jq -e '
|
|
.apiVersion == "monitoring.coreos.com/v1" and
|
|
.kind == "ServiceMonitor" and
|
|
.metadata.name == "gitea" and
|
|
.metadata.namespace == "gitea" and
|
|
.metadata.labels["observability.hyeonworks.com/instance"] == "home" and
|
|
.spec.jobLabel == "app.kubernetes.io/name" and
|
|
.spec.selector.matchLabels == {
|
|
"app.kubernetes.io/instance": "gitea",
|
|
"app.kubernetes.io/name": "gitea"
|
|
} and
|
|
.spec.endpoints == [{
|
|
"interval": "30s",
|
|
"port": "http",
|
|
"scrapeTimeout": "10s"
|
|
}]
|
|
' >/dev/null; then
|
|
fail "${gitea_profile} ServiceMonitor contract is not exact"
|
|
fi
|
|
|
|
for restricted_setting in \
|
|
'allowPrivilegeEscalation: false' \
|
|
'runAsNonRoot: true' \
|
|
'type: RuntimeDefault' \
|
|
'- ALL'; do
|
|
[[ "$(rg --count-matches --fixed-strings -- "$restricted_setting" \
|
|
"$gitea_profile_deployment_document" || true)" == "4" ]] || \
|
|
fail "${gitea_profile} must apply ${restricted_setting} to all four containers"
|
|
done
|
|
done
|
|
|
|
for forbidden_baseline_marker in \
|
|
'gitea-keycloak-oidc' \
|
|
'id.learn.hyeonworks.com' \
|
|
'gitea-allow-host-nginx-keycloak' \
|
|
'gitea-branding-assets' \
|
|
'gitea-branding-templates' \
|
|
'ALLOW_ONLY_EXTERNAL_REGISTRATION=true'; do
|
|
if rg --quiet --fixed-strings -- "$forbidden_baseline_marker" "${render_dir}/gitea.yaml"; then
|
|
fail "the baseline Gitea render contains OIDC-only marker ${forbidden_baseline_marker}"
|
|
fi
|
|
done
|
|
for baseline_setting in \
|
|
'DISABLE_REGISTRATION=true' \
|
|
'ALLOW_ONLY_EXTERNAL_REGISTRATION=false' \
|
|
'SHOW_REGISTRATION_BUTTON=false' \
|
|
'ENABLE_PASSWORD_SIGNIN_FORM=true'; do
|
|
[[ "$(rg --count-matches --fixed-strings -- "$baseline_setting" \
|
|
"${render_dir}/gitea.yaml" || true)" == "1" ]] || \
|
|
fail "the baseline Gitea render must contain exactly one ${baseline_setting} setting"
|
|
done
|
|
|
|
gitea_deployment_document="${render_dir}/gitea-oidc-deployment.yaml"
|
|
gitea_keycloak_policy_document="${render_dir}/gitea-keycloak-egress-policy.yaml"
|
|
gitea_branding_assets_document="${render_dir}/gitea-branding-assets-configmap.yaml"
|
|
gitea_branding_templates_document="${render_dir}/gitea-branding-templates-configmap.yaml"
|
|
|
|
extract_rendered_document \
|
|
"${render_dir}/gitea-oidc.yaml" \
|
|
Deployment \
|
|
gitea \
|
|
"$gitea_deployment_document" || \
|
|
fail "the Gitea render must contain exactly one gitea Deployment"
|
|
extract_rendered_document \
|
|
"${render_dir}/gitea-oidc.yaml" \
|
|
NetworkPolicy \
|
|
gitea-allow-host-nginx-keycloak \
|
|
"$gitea_keycloak_policy_document" || \
|
|
fail "the Gitea render must contain exactly one dedicated Keycloak egress NetworkPolicy"
|
|
|
|
# Assert only references to the externally-created OIDC credential Secret.
|
|
# No credential payload is rendered, decoded, read, or printed by these checks.
|
|
rg --quiet --multiline \
|
|
'(?s)- name: GITEA_OAUTH_KEY_0\n[[:space:]]+valueFrom:\n[[:space:]]+secretKeyRef:\n[[:space:]]+key: key\n[[:space:]]+name: gitea-keycloak-oidc' \
|
|
"$gitea_deployment_document" || \
|
|
fail "the Gitea Deployment does not reference gitea-keycloak-oidc key=key"
|
|
rg --quiet --multiline \
|
|
'(?s)- name: GITEA_OAUTH_SECRET_0\n[[:space:]]+valueFrom:\n[[:space:]]+secretKeyRef:\n[[:space:]]+key: secret\n[[:space:]]+name: gitea-keycloak-oidc' \
|
|
"$gitea_deployment_document" || \
|
|
fail "the Gitea Deployment does not reference gitea-keycloak-oidc key=secret"
|
|
[[ "$(rg --count-matches --fixed-strings \
|
|
'https://id.learn.hyeonworks.com/realms/hyeonworks/.well-known/openid-configuration' \
|
|
"${render_dir}/gitea-oidc.yaml" || true)" == "2" ]] || \
|
|
fail "the Gitea OAuth add/update script must use the exact Keycloak discovery URL"
|
|
|
|
# These are non-sensitive app.ini policy values rendered by the pinned Chart.
|
|
for expected_setting in \
|
|
'ALLOW_ONLY_EXTERNAL_REGISTRATION=true' \
|
|
'DISABLE_REGISTRATION=false' \
|
|
'SHOW_REGISTRATION_BUTTON=false' \
|
|
'ENABLE_PASSWORD_SIGNIN_FORM=true' \
|
|
'ENABLE_AUTO_REGISTRATION=true' \
|
|
'USERNAME=preferred_username' \
|
|
'ACCOUNT_LINKING=login' \
|
|
'OPENID_CONNECT_SCOPES=profile email'; do
|
|
[[ "$(rg --count-matches --fixed-strings "$expected_setting" \
|
|
"${render_dir}/gitea-oidc.yaml" || true)" == "1" ]] || \
|
|
fail "the Gitea render must contain exactly one ${expected_setting} setting"
|
|
done
|
|
|
|
if rg --quiet '^[[:space:]]*hostAliases:' "$gitea_deployment_document"; then
|
|
fail "the Gitea Deployment must rely on CoreDNS and must not contain hostAliases"
|
|
fi
|
|
|
|
[[ "$(rg --count-matches \
|
|
'^[[:space:]]*cidr:[[:space:]]*192\.168\.0\.107/32[[:space:]]*$' \
|
|
"$gitea_keycloak_policy_document" || true)" == "1" ]] || \
|
|
fail "the dedicated Keycloak egress policy must allow exactly 192.168.0.107/32"
|
|
[[ "$(rg --count-matches \
|
|
'^[[:space:]]*-[[:space:]]*port:[[:space:]]*443[[:space:]]*$' \
|
|
"$gitea_keycloak_policy_document" || true)" == "1" ]] || \
|
|
fail "the dedicated Keycloak egress policy must allow exactly TCP port 443"
|
|
[[ "$(rg --count-matches \
|
|
'^[[:space:]]*protocol:[[:space:]]*TCP[[:space:]]*$' \
|
|
"$gitea_keycloak_policy_document" || true)" == "1" ]] || \
|
|
fail "the dedicated Keycloak egress policy must use TCP"
|
|
rg --quiet \
|
|
'^[[:space:]]*-[[:space:]]*Egress[[:space:]]*$' \
|
|
"$gitea_keycloak_policy_document" || \
|
|
fail "the dedicated Keycloak NetworkPolicy must select egress traffic"
|
|
|
|
gitea_branding_assets_configmap_name="$(
|
|
(rg --only-matching --no-filename \
|
|
'gitea-branding-assets-[a-z0-9]+' \
|
|
"${render_dir}/gitea-oidc.yaml" || true) |
|
|
LC_ALL=C sort --unique
|
|
)"
|
|
gitea_branding_templates_configmap_name="$(
|
|
(rg --only-matching --no-filename \
|
|
'gitea-branding-templates-[a-z0-9]+' \
|
|
"${render_dir}/gitea-oidc.yaml" || true) |
|
|
LC_ALL=C sort --unique
|
|
)"
|
|
[[ "$gitea_branding_assets_configmap_name" =~ ^gitea-branding-assets-[a-z0-9]{10}$ ]] || \
|
|
fail "the Gitea branding assets ConfigMap must have one Kustomize content hash"
|
|
[[ "$gitea_branding_templates_configmap_name" =~ ^gitea-branding-templates-[a-z0-9]{10}$ ]] || \
|
|
fail "the Gitea branding templates ConfigMap must have one Kustomize content hash"
|
|
|
|
extract_rendered_document \
|
|
"${render_dir}/gitea-oidc.yaml" \
|
|
ConfigMap \
|
|
"$gitea_branding_assets_configmap_name" \
|
|
"$gitea_branding_assets_document" || \
|
|
fail "the Gitea render must contain exactly one branding assets ConfigMap"
|
|
extract_rendered_document \
|
|
"${render_dir}/gitea-oidc.yaml" \
|
|
ConfigMap \
|
|
"$gitea_branding_templates_configmap_name" \
|
|
"$gitea_branding_templates_document" || \
|
|
fail "the Gitea render must contain exactly one branding templates ConfigMap"
|
|
|
|
for asset_key in hyeonworks.css logo.svg favicon.svg; do
|
|
[[ "$(rg --count-matches \
|
|
"^[[:space:]]{2}${asset_key//./\\.}:[[:space:]]*\\|[+-]?[[:space:]]*$" \
|
|
"$gitea_branding_assets_document" || true)" == "1" ]] || \
|
|
fail "the branding assets ConfigMap must contain exactly one ${asset_key}"
|
|
done
|
|
for template_key in header.tmpl extra_links.tmpl; do
|
|
[[ "$(rg --count-matches \
|
|
"^[[:space:]]{2}${template_key//./\\.}:[[:space:]]*\\|[+-]?[[:space:]]*$" \
|
|
"$gitea_branding_templates_document" || true)" == "1" ]] || \
|
|
fail "the branding templates ConfigMap must contain exactly one ${template_key}"
|
|
done
|
|
|
|
[[ "$(rg --count-matches --fixed-strings \
|
|
"$gitea_branding_assets_configmap_name" \
|
|
"$gitea_deployment_document" || true)" == "1" ]] || \
|
|
fail "the Gitea Deployment must reference the hashed branding assets ConfigMap once"
|
|
[[ "$(rg --count-matches --fixed-strings \
|
|
"$gitea_branding_templates_configmap_name" \
|
|
"$gitea_deployment_document" || true)" == "1" ]] || \
|
|
fail "the Gitea Deployment must reference the hashed branding templates ConfigMap once"
|
|
rg --quiet --multiline \
|
|
'(?s)- mountPath: /data/gitea/public/assets\n[[:space:]]+name: branding-assets\n[[:space:]]+readOnly: true' \
|
|
"$gitea_deployment_document" || \
|
|
fail "the Gitea branding assets must be mounted read-only at the official custom path"
|
|
rg --quiet --multiline \
|
|
'(?s)- mountPath: /data/gitea/templates/custom\n[[:space:]]+name: branding-templates\n[[:space:]]+readOnly: true' \
|
|
"$gitea_deployment_document" || \
|
|
fail "the Gitea branding templates must be mounted read-only at the official custom path"
|
|
|
|
if rg --quiet '^[[:space:]]*type:[[:space:]]*(NodePort|LoadBalancer)[[:space:]]*$' \
|
|
"${render_dir}/platform-postgres.yaml" \
|
|
"${render_dir}/gitea.yaml" \
|
|
"${render_dir}/gitea-oidc.yaml"; then
|
|
fail "an application service is exposed as NodePort or LoadBalancer"
|
|
fi
|
|
|
|
if rg --quiet '^[[:space:]]*name:[[:space:]]*gitea-ssh[[:space:]]*$' \
|
|
"${render_dir}/gitea.yaml" "${render_dir}/gitea-oidc.yaml"; then
|
|
fail "the disabled Gitea SSH Service is still rendered"
|
|
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 six manifest files"
|
|
printf 'Preserved six verified manifests for the apply handoff.\n'
|
|
fi
|
|
|
|
printf 'Phase 1 baseline and Gitea OIDC desired rendering invariants passed.\n'
|
|
printf 'Temporary rendered manifests and generated chart caches will be removed on exit.\n'
|