#!/usr/bin/env bash set -euo pipefail SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) KNOE_HOME=${KNOE_HOME:-$(cd "$SCRIPT_DIR/.." && pwd)} CNPG_TEST_NAMESPACE=${CNPG_TEST_NAMESPACE:-knoe-cnpg-test} CNPG_CLUSTER_NAME=${CNPG_CLUSTER_NAME:-knoe-db} CNPG_OPERATOR_NAMESPACE=${CNPG_OPERATOR_NAMESPACE:-cnpg-system} CNPG_CLUSTER_MANIFEST=${CNPG_CLUSTER_MANIFEST:-$KNOE_HOME/k8s/knoe/knoe-db.yaml} CNPG_USER_SECRET_FILE=${CNPG_USER_SECRET_FILE:-$KNOE_HOME/knoe-db/knoe-db-user-secret.yaml} CNPG_WAIT_TIMEOUT=${CNPG_WAIT_TIMEOUT:-300} CNPG_FALLBACK_VERSION=${CNPG_FALLBACK_VERSION:-1.27.0} SKIP_BUILD=${SKIP_BUILD:-false} IMAGE_REPO=${IMAGE_REPO:-knoe-db} IMAGE_LOAD=${IMAGE_LOAD:-} IMAGE_LOAD_CMD=${IMAGE_LOAD_CMD:-} log() { printf "[%s] %s\n" "$(date +%H:%M:%S)" "$*" } require_tool() { command -v "$1" >/dev/null 2>&1 || { echo "Missing required tool: $1" >&2; exit 1; } } get_latest_cnpg_version() { local version="" if command -v python3 >/dev/null 2>&1; then version=$(python3 - <<'PY' import json,sys,urllib.request try: with urllib.request.urlopen("https://api.github.com/repos/cloudnative-pg/cloudnative-pg/releases/latest", timeout=10) as resp: data=json.load(resp) tag=data.get("tag_name", "") print(tag.lstrip("v")) except Exception: pass PY ) || true fi if [[ -z "$version" ]]; then version=$(curl -fsSL "https://api.github.com/repos/cloudnative-pg/cloudnative-pg/releases/latest" 2>/dev/null \ | sed -n 's/.*"tag_name": *"v\{0,1\}\([^"]*\)".*/\1/p' | head -n1) || true fi if [[ -z "$version" ]]; then echo "$CNPG_FALLBACK_VERSION" else echo "$version" fi } resolve_cnpg_manifest_url() { if [[ -n "${CNPG_MANIFEST_URL:-}" ]]; then echo "$CNPG_MANIFEST_URL" return 0 fi local version version=${CNPG_VERSION:-$(get_latest_cnpg_version)} local minor minor=$(echo "$version" | awk -F. '{print $1"."$2}') echo "https://raw.githubusercontent.com/cloudnative-pg/cloudnative-pg/release-${minor}/releases/cnpg-${version}.yaml" } compute_image_tag() { local pg_version release pg_version=$(tr -d '[:space:]' < "$KNOE_HOME/conf/postgresql/.version" 2>/dev/null || true) release=$(tr -d '[:space:]' < "$KNOE_HOME/knoe-db/.version" 2>/dev/null || true) if [[ -z "$pg_version" ]]; then pg_version="17.7" fi if [[ -z "$release" ]]; then release="000" fi if [[ "$release" =~ ^[0-9]+$ ]]; then release=$(printf "%03d" "$release") fi echo "${pg_version}-${release}" } ensure_namespace() { if ! kubectl get namespace "$CNPG_TEST_NAMESPACE" >/dev/null 2>&1; then log "Creating namespace $CNPG_TEST_NAMESPACE" kubectl create namespace "$CNPG_TEST_NAMESPACE" >/dev/null fi } ensure_cnpg_operator() { if kubectl get crd clusters.postgresql.cnpg.io >/dev/null 2>&1; then log "CNPG CRDs detected; operator install skipped." return 0 fi local url url=$(resolve_cnpg_manifest_url) log "Installing CloudNative-PG operator from: $url" kubectl apply --server-side -f "$url" if kubectl -n "$CNPG_OPERATOR_NAMESPACE" get deploy cnpg-controller-manager >/dev/null 2>&1; then kubectl -n "$CNPG_OPERATOR_NAMESPACE" rollout status deploy/cnpg-controller-manager --timeout=120s || true fi } build_image() { local image_tag image_tag=$(compute_image_tag) IMAGE="${IMAGE_REPO}:${image_tag}" if [[ "$SKIP_BUILD" == "true" ]]; then log "Skipping docker build (SKIP_BUILD=true)." return 0 fi log "Building image $IMAGE" docker build -t "$IMAGE" "$KNOE_HOME/knoe-db" } load_image_if_requested() { if [[ -n "$IMAGE_LOAD_CMD" ]]; then log "Loading image with custom command: $IMAGE_LOAD_CMD" eval "$IMAGE_LOAD_CMD" return 0 fi case "$IMAGE_LOAD" in kind) require_tool kind log "Loading image into kind: $IMAGE" kind load docker-image "$IMAGE" ;; minikube) require_tool minikube log "Loading image into minikube: $IMAGE" minikube image load "$IMAGE" ;; k3d) require_tool k3d log "Importing image into k3d: $IMAGE" if [[ -n "${K3D_CLUSTER_NAME:-}" ]]; then k3d image import "$IMAGE" -c "$K3D_CLUSTER_NAME" else k3d image import "$IMAGE" fi ;; "") ;; *) echo "Unknown IMAGE_LOAD value: $IMAGE_LOAD" >&2 exit 2 ;; esac } render_cluster_manifest() { local out_file=$1 if [[ ! -f "$CNPG_CLUSTER_MANIFEST" ]]; then echo "Cluster manifest not found: $CNPG_CLUSTER_MANIFEST" >&2 exit 1 fi awk -v image="$IMAGE" ' BEGIN { IGNORECASE=1 } /^ imageName:/ { print " imageName: " image; next } /^ instances:/ { print " instances: 3"; next } /postgis_tiger_geocoder/ { next } /schema tiger/ { next } { print } ' "$CNPG_CLUSTER_MANIFEST" > "$out_file" } collect_error_pod_logs() { local out_dir=$1 local pods pods=$(kubectl -n "$CNPG_TEST_NAMESPACE" get pods -l "cnpg.io/cluster=$CNPG_CLUSTER_NAME" --no-headers 2>/dev/null \ | awk '$3 ~ /(Error|CrashLoopBackOff|Init:Error|Init:CrashLoopBackOff)/ {print $1}') if [[ -z "$pods" ]]; then log "No error pods detected." return 0 fi mkdir -p "$out_dir" for pod in $pods; do kubectl -n "$CNPG_TEST_NAMESPACE" describe pod "$pod" > "$out_dir/${pod}.describe" 2>&1 || true kubectl -n "$CNPG_TEST_NAMESPACE" logs "$pod" --all-containers --prefix=true --timestamps=true > "$out_dir/${pod}.log" 2>&1 || true kubectl -n "$CNPG_TEST_NAMESPACE" logs "$pod" --all-containers --prefix=true --timestamps=true --previous > "$out_dir/${pod}.previous.log" 2>&1 || true done log "Error pod logs written to $out_dir" } wait_for_primary_ready() { log "Waiting for CNPG primary pod to be Ready (timeout ${CNPG_WAIT_TIMEOUT}s)..." local deadline deadline=$(( $(date +%s) + CNPG_WAIT_TIMEOUT )) while [[ $(date +%s) -lt $deadline ]]; do if kubectl -n "$CNPG_TEST_NAMESPACE" get pods \ -l "cnpg.io/cluster=$CNPG_CLUSTER_NAME,cnpg.io/instanceRole=primary" \ --no-headers 2>/dev/null | grep -q .; then if kubectl -n "$CNPG_TEST_NAMESPACE" wait --for=condition=Ready pod \ -l "cnpg.io/cluster=$CNPG_CLUSTER_NAME,cnpg.io/instanceRole=primary" \ --timeout=30s; then return 0 fi fi sleep 5 done return 1 } main() { require_tool kubectl require_tool docker build_image load_image_if_requested ensure_namespace ensure_cnpg_operator if [[ -f "$CNPG_USER_SECRET_FILE" ]]; then log "Applying CNPG user secret: $CNPG_USER_SECRET_FILE" kubectl -n "$CNPG_TEST_NAMESPACE" apply -f "$CNPG_USER_SECRET_FILE" else log "Skipping CNPG user secret (file not found): $CNPG_USER_SECRET_FILE" fi local tmp_dir tmp_dir=$(mktemp -d) local rendered_manifest="$tmp_dir/cluster.yaml" render_cluster_manifest "$rendered_manifest" log "Applying CNPG cluster manifest: $rendered_manifest" kubectl -n "$CNPG_TEST_NAMESPACE" apply -f "$rendered_manifest" if ! wait_for_primary_ready; then local log_dir local base_log_dir base_log_dir="${PROLE_LOGS:-$KNOE_HOME/logs}" base_log_dir="${base_log_dir%/}" log_dir=${CNPG_TEST_LOG_DIR:-$base_log_dir/cnpg-test-$(date +%Y%m%dT%H%M%S)} collect_error_pod_logs "$log_dir" kubectl -n "$CNPG_TEST_NAMESPACE" get pods -l "cnpg.io/cluster=$CNPG_CLUSTER_NAME" || true echo "CNPG primary pod did not become Ready within timeout." >&2 exit 1 fi kubectl -n "$CNPG_TEST_NAMESPACE" get pods -l "cnpg.io/cluster=$CNPG_CLUSTER_NAME" || true log "CNPG test completed successfully." } main "$@"