Add platform infrastructure configuration
This commit is contained in:
Executable
+671
@@ -0,0 +1,671 @@
|
||||
#!/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'
|
||||
Reference in New Issue
Block a user