diff --git a/scripts/cleanup-supabase-pvs.sh b/scripts/cleanup-supabase-pvs.sh index 517d87e..743fcd1 100755 --- a/scripts/cleanup-supabase-pvs.sh +++ b/scripts/cleanup-supabase-pvs.sh @@ -1,44 +1,109 @@ #!/usr/bin/env bash set -euo pipefail -PROJECT="plenary-truck-485623-p7" +PROJECT="${PROJECT:-${GCP_PROJECT_ID:-${GOOGLE_CLOUD_PROJECT:-plenary-truck-485623-p7}}}" +DRY_RUN_RAW="${SUPABASE_RETAINED_PV_CLEANUP_DRY_RUN:-false}" +DRY_RUN="false" +if [[ "${DRY_RUN_RAW,,}" =~ ^(1|true|yes|on)$ ]]; then + DRY_RUN="true" +fi -echo "==> Finding stale Supabase PVs..." +echo "==> Finding stale Supabase PVs (Released + Retain, Supabase namespace/storageClass)..." +echo "==> Dry run: ${DRY_RUN}" +echo "==> Project: ${PROJECT}" -kubectl get pv -o json -| jq -r ' - .items[] - | select( - .status.phase=="Released" - and .spec.storageClassName=="supabase-standard" - ) - | [ - .metadata.name, - .spec.csi.volumeHandle - ] - | @tsv -' | while IFS=$'\t' read -r PV DISK; do +mapfile -t CANDIDATES < <( + kubectl get pv -o json | jq -r ' + .items[] + | . as $pv + | ($pv.spec.claimRef.namespace // "") as $claimNs + | ($pv.spec.storageClassName // "") as $sc + | select( + ($pv.status.phase == "Released") + and (($pv.spec.persistentVolumeReclaimPolicy // "") == "Retain") + and ( + $sc == "supabase-standard" + or $claimNs == "supabase" + ) + ) + | ( + ($pv.spec.csi.volumeHandle // $pv.spec.gcePersistentDisk.pdName // "") + ) as $handle + | ( + ($pv.metadata.labels["topology.kubernetes.io/zone"] + // $pv.metadata.labels["failure-domain.beta.kubernetes.io/zone"] + // $pv.spec.csi.volumeAttributes["topology.gke.io/zone"] + // ( + if ($handle | test("/zones/[^/]+/disks/[^/]+$")) + then ($handle | capture("/zones/(?[^/]+)/disks/").z) + else "" + end + ) + ) + ) as $zone + | [ + ($pv.metadata.name // ""), + $claimNs, + ($pv.spec.claimRef.name // ""), + ($pv.status.phase // ""), + ($pv.spec.persistentVolumeReclaimPolicy // ""), + $sc, + $handle, + $zone + ] + | @tsv + ' +) - if [[ -z "$DISK" || "$DISK" == "null" ]]; then - echo "[WARN] PV $PV has no disk handle, skipping" +if [[ ${#CANDIDATES[@]} -eq 0 ]]; then + echo "==> No stale Supabase Released/Retain PV candidates found." + exit 0 +fi + +echo "==> Candidates:" +for row in "${CANDIDATES[@]}"; do + IFS=$'\t' read -r PV CLAIM_NS CLAIM_NAME PHASE RECLAIM SC DISK ZONE <<<"$row" + echo " - PV=${PV} claim=${CLAIM_NS}/${CLAIM_NAME:-} phase=${PHASE} reclaim=${RECLAIM} sc=${SC} disk=${DISK:-} zone=${ZONE:-}" +done + +for row in "${CANDIDATES[@]}"; do + IFS=$'\t' read -r PV CLAIM_NS CLAIM_NAME PHASE RECLAIM SC DISK ZONE <<<"$row" + NAME="${DISK##*/}" + + if [[ "$DRY_RUN" == "true" ]]; then + echo "[dry-run] kubectl delete pv ${PV} --ignore-not-found" + if [[ -n "$DISK" && "$DISK" != "null" ]]; then + if [[ -n "$ZONE" ]]; then + echo "[dry-run] gcloud compute disks delete ${NAME} --zone ${ZONE} --project ${PROJECT} --quiet" + else + echo "[dry-run] skip disk delete for ${DISK} (missing zone)" + fi + else + echo "[dry-run] skip disk delete for PV ${PV} (no disk handle)" + fi continue fi - # extract zone + disk name - ZONE=$(echo "$DISK" | awk -F/ '{print $(NF-2)}') - NAME=$(echo "$DISK" | awk -F/ '{print $NF}') - - echo "==> Cleaning PV=$PV disk=$NAME zone=$ZONE" - - echo " -> deleting PV" + echo "==> Deleting PV ${PV}" kubectl delete pv "$PV" --ignore-not-found - echo " -> deleting GCE disk" - gcloud compute disks delete "$NAME" - --zone "$ZONE" - --project "$PROJECT" - --quiet || echo "[WARN] disk delete failed: $NAME" + if [[ -z "$DISK" || "$DISK" == "null" ]]; then + echo "[WARN] PV ${PV} has no disk handle; skipped disk delete" + continue + fi + if [[ -z "$ZONE" ]]; then + echo "[WARN] Disk ${DISK} zone is unknown; skipped disk delete" + continue + fi + if ! command -v gcloud >/dev/null 2>&1; then + echo "[WARN] gcloud not found; skipped disk delete for ${DISK}" + continue + fi + + echo "==> Deleting GCE disk ${NAME} (zone=${ZONE})" + gcloud compute disks delete "$NAME" --zone "$ZONE" --project "$PROJECT" --quiet \ + || echo "[WARN] disk delete failed: ${NAME}" done echo "==> Cleanup complete"