#!/usr/bin/env bash set -Eeuo pipefail # Do not allow a caller's `bash -x` setting to expose prompts or future secrets. set +x readonly EXPECTED_HELM_VERSION="v3.19.4" readonly TARGET_NODE="donghyeon-system-product-name" readonly REPOSITORY_ROOT="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." && pwd -P)" readonly -a VERIFIED_MANIFEST_NAMES=( namespaces ssd-local-pv cnpg-operator platform-postgres gitea gitea-oidc ) fail() { printf 'ERROR: %s\n' "$*" >&2 exit 1 } check_create_only_state() { if kubectl --namespace gitea get deployment/gitea >/dev/null 2>&1; then fail "Phase 1 is create-only and gitea/gitea already exists; use a dedicated Gitea lifecycle script" fi if kubectl --namespace gitea get secret/gitea-keycloak-oidc >/dev/null 2>&1; then fail "Phase 1 baseline is blocked because gitea/gitea-keycloak-oidc already exists" fi } usage() { cat <<'USAGE' Usage: bash scripts/bootstrap/apply-phase1-gitea.sh --execute Applies the create-only Gitea baseline in dependency order. It does not modify Host Nginx, configure Keycloak OIDC, or install Argo CD. If a Gitea Deployment or OIDC Secret already exists, use the dedicated lifecycle scripts instead. USAGE } [[ "${1:-}" == "--execute" && "$#" -eq 1 ]] || { usage exit 2 } for command_name in kubectl curl rg sha256sum stat; 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 [[ "$("$HELM_BIN" version --template '{{.Version}}')" == "$EXPECTED_HELM_VERSION" ]] || \ fail "Helm must be exactly ${EXPECTED_HELM_VERSION}" render_temp_dir="$(mktemp -d /tmp/platform-phase1-apply.XXXXXX)" cleanup() { case "$render_temp_dir" in /tmp/platform-phase1-apply.*) rm -rf -- "$render_temp_dir" ;; *) printf 'WARNING: refusing to remove unexpected render directory: %s\n' \ "$render_temp_dir" >&2 ;; esac } trap cleanup EXIT trap 'exit 130' INT trap 'exit 143' TERM cd -- "$REPOSITORY_ROOT" PLATFORM_HELM_BIN="$HELM_BIN" bash scripts/validate/render-phase1.sh \ --verified-output-dir "$render_temp_dir" declare -A verified_manifest_sha256=() for manifest_name in "${VERIFIED_MANIFEST_NAMES[@]}"; do manifest_path="${render_temp_dir}/${manifest_name}.yaml" [[ -f "$manifest_path" && ! -L "$manifest_path" && -O "$manifest_path" && -s "$manifest_path" ]] || \ fail "verified manifest is missing or unsafe: ${manifest_path}" [[ "$(stat --format='%a' -- "$manifest_path")" == "600" ]] || \ fail "verified manifest must have mode 0600: ${manifest_path}" checksum_output="$(sha256sum -- "$manifest_path")" verified_manifest_sha256["$manifest_name"]="${checksum_output%% *}" done verified_entry_count="$(find "$render_temp_dir" -mindepth 1 -maxdepth 1 | wc -l | tr -d '[:space:]')" [[ "$verified_entry_count" == "${#VERIFIED_MANIFEST_NAMES[@]}" ]] || \ fail "verified handoff must contain exactly ${#VERIFIED_MANIFEST_NAMES[@]} manifest files" verify_manifest_unchanged() { local manifest_name="$1" local manifest_path="${render_temp_dir}/${manifest_name}.yaml" local checksum_output local actual_sha256 [[ -f "$manifest_path" && ! -L "$manifest_path" && -O "$manifest_path" && -s "$manifest_path" ]] || \ fail "verified manifest became missing or unsafe: ${manifest_path}" checksum_output="$(sha256sum -- "$manifest_path")" actual_sha256="${checksum_output%% *}" [[ "$actual_sha256" == "${verified_manifest_sha256[$manifest_name]}" ]] || \ fail "verified manifest changed before apply: ${manifest_name}.yaml" } current_context="$(kubectl config current-context)" api_server="$(kubectl config view --minify --output=jsonpath='{.clusters[0].cluster.server}')" kubectl get node "$TARGET_NODE" >/dev/null 2>&1 || \ fail "target node is missing from the selected cluster: ${TARGET_NODE}" check_create_only_state printf '\nKubernetes context: %s\nAPI server: %s\nTarget node: %s\n' \ "$current_context" "$api_server" "$TARGET_NODE" printf 'Type APPLY %s to start the cluster mutation: ' "$current_context" read -r confirmation [[ "$confirmation" == "APPLY ${current_context}" ]] || fail "cancelled" [[ "$(kubectl config current-context)" == "$current_context" ]] || \ fail "kubectl context changed after confirmation" confirmed_api_server="$(kubectl config view --minify --output=jsonpath='{.clusters[0].cluster.server}')" [[ "$confirmed_api_server" == "$api_server" ]] || \ fail "Kubernetes API server changed after confirmation" kubectl get node "$TARGET_NODE" >/dev/null 2>&1 || \ fail "target node disappeared after confirmation: ${TARGET_NODE}" check_create_only_state for manifest_name in "${VERIFIED_MANIFEST_NAMES[@]}"; do verify_manifest_unchanged "$manifest_name" done printf '\n[1/8] Preparing exact SSD Local PV directories\n' bash scripts/bootstrap/prepare-ssd-local-paths.sh printf '\n[2/8] Applying Phase 1 namespaces\n' verify_manifest_unchanged namespaces kubectl apply --filename="${render_temp_dir}/namespaces.yaml" printf '\n[3/8] Applying static SSD StorageClasses and Local PVs\n' verify_manifest_unchanged ssd-local-pv kubectl apply --filename="${render_temp_dir}/ssd-local-pv.yaml" printf '\n[4/8] Installing CloudNativePG CRDs and operator\n' verify_manifest_unchanged cnpg-operator kubectl apply --server-side --filename="${render_temp_dir}/cnpg-operator.yaml" kubectl wait --for=condition=Established \ customresourcedefinition/clusters.postgresql.cnpg.io \ customresourcedefinition/databaseroles.postgresql.cnpg.io \ customresourcedefinition/databases.postgresql.cnpg.io \ --timeout=3m kubectl --namespace cnpg-system wait --for=condition=Available deployment \ --selector=app.kubernetes.io/name=cloudnative-pg --timeout=5m printf '\n[5/8] Ensuring or reusing the three Secret contracts\n' bash scripts/bootstrap/create-phase1-secrets.sh --execute printf '\n[6/8] Applying the shared platform PostgreSQL resources\n' verify_manifest_unchanged platform-postgres kubectl apply --server-side --filename="${render_temp_dir}/platform-postgres.yaml" kubectl --namespace platform-data wait --for=condition=Ready \ cluster/platform-postgres --timeout=10m kubectl --namespace platform-data wait \ --for=jsonpath='{.status.applied}'=true \ database/platform-postgres-gitea --timeout=3m printf '\n[7/8] Applying the Gitea baseline, PVC, policies, and HTTP Ingress\n' verify_manifest_unchanged gitea kubectl apply --filename="${render_temp_dir}/gitea.yaml" kubectl --namespace gitea rollout status deployment/gitea --timeout=10m kubectl --namespace gitea wait \ --for=jsonpath='{.endpoints[0].conditions.ready}'=true \ endpointslice \ --selector=kubernetes.io/service-name=gitea-http \ --timeout=2m printf '\n[8/8] Checking Host-based routing through Traefik HTTP NodePort\n' curl --fail-with-body --show-error \ --retry 24 \ --retry-all-errors \ --retry-connrefused \ --retry-delay 5 \ --retry-max-time 120 \ --max-time 10 \ --header 'Host: git.learn.hyeonworks.com' \ http://127.0.0.1:30080/api/healthz printf '\nPhase 1 cluster resources are ready. Host Nginx was not changed.\n' printf 'Review infrastructure/networking/host-nginx/README.md for the final cutover.\n'