196 lines
7.4 KiB
Bash
Executable File
196 lines
7.4 KiB
Bash
Executable File
#!/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'
|