#!/usr/bin/env bash set -euo pipefail # init_postgrest.sh # Purpose: # - Deploy PostgREST (postgrest/postgrest) into the knoe-db namespace # - Creates the knoe-db-postgrest-secrets k8s secret (postgres password + JWT secret) # - Applies the postgrest deployment and service manifests # - Provides start/stop/status/restart actions # # Usage: # ./init_postgrest.sh [--mode MODE] [-n NAMESPACE] SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) # shellcheck disable=SC1090 source "$SCRIPT_DIR/knoe_cfg.sh" if [[ "${1:-}" == "--mode" || "${1:-}" == "-m" ]]; then knoe_set_mode "${2:-}" shift 2 elif [[ "${1:-}" == --mode=* || "${1:-}" == -m=* ]]; then knoe_set_mode "${1#*=}" shift fi NAMESPACE="${PROLE_NAMESPACE}" while [[ $# -gt 0 ]]; do case "$1" in -n|--namespace) shift if [[ -z "${1:-}" ]]; then echo "ERROR: -n/--namespace requires a value" >&2 exit 2 fi NAMESPACE="$1" ;; -n=*|--namespace=*) NAMESPACE="${1#*=}" ;; start|stop|status|restart) ACTION="$1" ;; -h|--help) usage ;; *) echo "ERROR: Unknown argument: $1" >&2 exit 2 ;; esac shift done ACTION="${ACTION:-start}" KNOE_HOME=${KNOE_HOME:-$(cd "$SCRIPT_DIR/.." && pwd)} POSTGREST_IMAGE="${POSTGREST_IMAGE:-postgrest/postgrest:v14.5}" POSTGREST_NAME="${POSTGREST_NAME:-knoe-db-postgrest}" POSTGREST_PORT="${POSTGREST_PORT:-3000}" KNOE_DB_SERVICE="${KNOE_DB_SERVICE:-knoe-db-postgres}" POSTGREST_SECRET_NAME="${POSTGREST_SECRET_NAME:-knoe-db-postgrest-secrets}" usage() { cat < Actions: start Create secrets and deploy PostgREST to the knoe-db namespace stop Remove PostgREST deployment and secrets status Show PostgREST pod/service status restart Restart PostgREST pods USAGE exit 1 } ensure_tools() { for t in kubectl; do command -v "$t" >/dev/null || { echo "Missing required tool: $t" >&2; exit 1; } done } ensure_namespace() { if ! kubectl get namespace "$NAMESPACE" >/dev/null 2>&1; then echo "Creating namespace '$NAMESPACE' ..." kubectl create namespace "$NAMESPACE" >/dev/null 2>&1 || true fi } resolve_postgres_password() { # Try to read the postgres password from the CloudNativePG superuser secret local cnpg_secret="knoe-db-superuser" local pw="" if kubectl get secret "$cnpg_secret" -n "$NAMESPACE" >/dev/null 2>&1; then pw=$(kubectl get secret "$cnpg_secret" -n "$NAMESPACE" -o jsonpath='{.data.password}' 2>/dev/null | base64 -d 2>/dev/null || true) fi if [[ -z "$pw" ]]; then # Fallback: check for POSTGRES_PASSWORD env var or knoe.cfg pw="${POSTGRES_PASSWORD:-}" fi if [[ -z "$pw" ]]; then echo "ERROR: Cannot resolve postgres password. Ensure knoe-db-superuser secret exists in namespace '$NAMESPACE' or set POSTGRES_PASSWORD." >&2 exit 1 fi echo "$pw" } resolve_jwt_secret() { # Try to read JWT secret from supabase namespace secrets local jwt="" if kubectl get secret supabase-jwt -n supabase >/dev/null 2>&1; then jwt=$(kubectl get secret supabase-jwt -n supabase -o jsonpath='{.data.jwt-secret}' 2>/dev/null | base64 -d 2>/dev/null || true) fi if [[ -z "$jwt" ]]; then # Fallback: try reading from supabase .env file local env_file="${DEV_HOME:-$HOME/dev}/supabase/docker/.env" if [[ -f "$env_file" ]]; then jwt=$(grep -E '^JWT_SECRET=' "$env_file" | head -1 | cut -d= -f2- | tr -d "'\"" || true) fi fi if [[ -z "$jwt" ]]; then jwt="${JWT_SECRET:-}" fi if [[ -z "$jwt" ]]; then echo "ERROR: Cannot resolve JWT secret. Set JWT_SECRET or ensure supabase-jwt secret exists." >&2 exit 1 fi echo "$jwt" } setup_database_roles() { echo "Ensuring PostgREST database roles and schemas exist ..." local pg_password pg_password=$(resolve_postgres_password) # Find primary pod local primary primary=$(kubectl -n "$NAMESPACE" get pods -l "cnpg.io/cluster=knoe-db,cnpg.io/instanceRole=primary" -o jsonpath='{.items[0].metadata.name}' 2>/dev/null || true) if [[ -z "$primary" ]]; then primary=$(kubectl -n "$NAMESPACE" get pods -l "cnpg.io/cluster=knoe-db" -o jsonpath='{.items[0].metadata.name}' 2>/dev/null || true) fi if [[ -z "$primary" ]]; then echo "WARNING: No knoe-db pod found in namespace '$NAMESPACE'; skipping role setup." >&2 return 0 fi kubectl -n "$NAMESPACE" exec "$primary" -c postgres -- psql -U postgres -d postgres -c " CREATE SCHEMA IF NOT EXISTS storage; CREATE SCHEMA IF NOT EXISTS graphql_public; DO \$\$ BEGIN IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname='anon') THEN CREATE ROLE anon NOLOGIN; END IF; END \$\$; DO \$\$ BEGIN IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname='authenticator') THEN CREATE ROLE authenticator LOGIN PASSWORD '${pg_password}'; END IF; END \$\$; GRANT USAGE ON SCHEMA public TO anon; GRANT USAGE ON SCHEMA storage TO anon; GRANT USAGE ON SCHEMA graphql_public TO anon; GRANT anon TO authenticator; " || echo "WARNING: Could not set up PostgREST roles (cluster may not be ready yet)." >&2 echo "PostgREST database roles and schemas ready." } create_secrets() { echo "Resolving secrets for PostgREST ..." local pg_password local jwt_secret pg_password=$(resolve_postgres_password) jwt_secret=$(resolve_jwt_secret) echo "Creating/updating secret '$POSTGREST_SECRET_NAME' in namespace '$NAMESPACE' ..." kubectl create secret generic "$POSTGREST_SECRET_NAME" \ --namespace="$NAMESPACE" \ --from-literal=postgres-password="$pg_password" \ --from-literal=jwt-secret="$jwt_secret" \ --dry-run=client -o yaml | kubectl apply -f - echo "Secret '$POSTGREST_SECRET_NAME' ready." } check_and_repair_pods() { echo "Checking for non-functioning $POSTGREST_NAME pods ..." local bad_pods bad_pods=$(kubectl get pods -n "$NAMESPACE" -l app="$POSTGREST_NAME" \ --field-selector='status.phase!=Running' -o jsonpath='{.items[*].metadata.name}' 2>/dev/null || true) # Also check Running pods that have containers not ready (CrashLoopBackOff, Error, etc.) local not_ready_pods not_ready_pods=$(kubectl get pods -n "$NAMESPACE" -l app="$POSTGREST_NAME" \ -o jsonpath='{range .items[*]}{.metadata.name}{" "}{range .status.containerStatuses[*]}{.ready}{" "}{end}{"\n"}{end}' 2>/dev/null | \ grep -v '^$' | grep 'false' | awk '{print $1}' || true) local all_bad all_bad=$(echo -e "${bad_pods}\n${not_ready_pods}" | sort -u | xargs) if [[ -n "$all_bad" ]]; then echo "Found non-functioning pods: $all_bad" echo "Deleting non-functioning pods to allow redeployment ..." for pod in $all_bad; do kubectl delete pod "$pod" -n "$NAMESPACE" --grace-period=0 --force 2>/dev/null || true done echo "Non-functioning pods removed." else echo "No non-functioning pods found." fi # If the deployment exists but is in a bad state, delete it so we can recreate cleanly if kubectl get deployment "$POSTGREST_NAME" -n "$NAMESPACE" >/dev/null 2>&1; then local available available=$(kubectl get deployment "$POSTGREST_NAME" -n "$NAMESPACE" \ -o jsonpath='{.status.availableReplicas}' 2>/dev/null || echo "0") if [[ "${available:-0}" == "0" ]]; then echo "Deployment '$POSTGREST_NAME' has no available replicas; deleting for clean redeployment ..." kubectl delete deployment "$POSTGREST_NAME" -n "$NAMESPACE" --ignore-not-found=true sleep 2 fi fi } deploy() { echo "Deploying $POSTGREST_NAME to namespace '$NAMESPACE' ..." local manifests_dir="$KNOE_HOME/deploy/opentofu/k3s/manifests/knoe" kubectl apply -f "$manifests_dir/postgrest-deployment.yaml" -n "$NAMESPACE" kubectl apply -f "$manifests_dir/postgrest-service.yaml" -n "$NAMESPACE" # Check if the backing database service has endpoints before waiting local db_endpoints db_endpoints=$(kubectl get endpoints "$KNOE_DB_SERVICE" -n "$NAMESPACE" -o jsonpath='{.subsets[*].addresses[*].ip}' 2>/dev/null || true) if [[ -z "$db_endpoints" ]]; then echo "WARNING: Database service '$KNOE_DB_SERVICE' has no ready endpoints." echo "PostgREST is deployed but will not become ready until the database is available." echo "PostgREST will automatically connect once the database is running." echo "$POSTGREST_NAME deployment applied (waiting for database)." return 0 fi echo "Waiting for $POSTGREST_NAME rollout ..." if kubectl rollout status deployment/"$POSTGREST_NAME" -n "$NAMESPACE" --timeout=120s; then echo "$POSTGREST_NAME deployed successfully." else echo "WARNING: $POSTGREST_NAME rollout did not complete within timeout." echo "The pod may still be waiting for the database to become available." echo "Check status with: $0 status" fi } stop() { echo "Removing $POSTGREST_NAME from namespace '$NAMESPACE' ..." kubectl delete deployment "$POSTGREST_NAME" -n "$NAMESPACE" --ignore-not-found=true kubectl delete service "$POSTGREST_NAME" -n "$NAMESPACE" --ignore-not-found=true kubectl delete secret "$POSTGREST_SECRET_NAME" -n "$NAMESPACE" --ignore-not-found=true echo "$POSTGREST_NAME removed." } status() { echo "=== $POSTGREST_NAME pods ===" kubectl get pods -n "$NAMESPACE" -l app="$POSTGREST_NAME" 2>/dev/null || echo "No pods found" echo "" echo "=== $POSTGREST_NAME service ===" kubectl get svc "$POSTGREST_NAME" -n "$NAMESPACE" 2>/dev/null || echo "No service found" } restart() { echo "Restarting $POSTGREST_NAME ..." kubectl rollout restart deployment/"$POSTGREST_NAME" -n "$NAMESPACE" kubectl rollout status deployment/"$POSTGREST_NAME" -n "$NAMESPACE" --timeout=120s echo "$POSTGREST_NAME restarted." } case "$ACTION" in start) ensure_tools ensure_namespace check_and_repair_pods setup_database_roles create_secrets deploy ;; stop) ensure_tools stop ;; status) ensure_tools status ;; restart) ensure_tools restart ;; *) usage ;; esac