chore: update GKE storage class and reconcile PVCs for Supabase deployment

- Changed default GKE non-DB PVCs to use CSI `pd-standard` with WFFC mode, deprecating legacy `standard`.
- Added reconciliation logic for APP PVCs to align with updated storage class.
- Enforced single replica for Supabase functions on GKE, disabling autoscaling.
- Updated tests to validate storage class changes, PVC reconciliation, and single-replica logic.
This commit is contained in:
chrisfu 2026-04-14 11:13:14 -07:00
parent fddaf80823
commit 174877201e
3 changed files with 89 additions and 41 deletions

View File

@ -107,10 +107,10 @@ app_values.setdefault("scheduling", {})["enforceGeneralNodeRole"] = False
clear_stale_app_scheduling(app_values)
db_values = deepcopy(base)
clear_stale_app_scheduling(db_values)
for component in (
"analytics",
"auth",
"functions",
"imgproxy",
"meta",
"minio",
@ -130,7 +130,13 @@ force_single_functions_replica = os.environ.get(
"SUPABASE_GKE_FORCE_FUNCTIONS_SINGLE_REPLICA", ""
).strip().lower() in {"1", "true", "yes", "on"}
if force_single_functions_replica:
app_values.setdefault("replicaCount", {})["functions"] = 1
deployment = app_values.setdefault("deployment", {})
functions_cfg = deployment.setdefault("functions", {})
functions_cfg["replicaCount"] = 1
autoscaling = app_values.setdefault("autoscaling", {})
functions_as = autoscaling.setdefault("functions", {})
functions_as["enabled"] = False
json.dump(app_values, open(app_path, "w", encoding="utf-8"), indent=2)
json.dump(db_values, open(db_path, "w", encoding="utf-8"), indent=2)
@ -2213,22 +2219,6 @@ run_helm() {
local db_frontdoor_release="${HELM_RELEASE}-frontdoor-db"
export SUPABASE_FRONTDOOR_DB_RELEASE="$db_frontdoor_release"
ensure_cross_cluster_db_host
helm_render_values
setup_knoe_db_for_supabase
local values="$PROJECT_ROOT/supabase/helm/generated/values.generated.json"
local db_frontdoor_manifest_path=""
local ns="supabase"
ns="$(load_manifest_summary_path supabase_namespace)"
ns="${ns:-supabase}"
db_frontdoor_manifest_path="$(load_manifest_summary_path manifests_frontdoor_db)"
if [[ "$HELM_TEMPLATE_ONLY" == "true" ]]; then
log "Helm template only; manifests ready in supabase/k8s"
return 0
fi
local storage_class
storage_class="${SUPABASE_STORAGE_CLASS:-}"
local first_node_name=""
@ -2267,6 +2257,26 @@ print(c.get('Global','SUPABASE_STORAGE_CLASS',fallback=''))" 2>/dev/null || true
force_gke_functions_single_replica="true"
fi
# Export these so the renderer and other sub-processes see the resolved GKE/storage state
export SUPABASE_GKE_FORCE_FUNCTIONS_SINGLE_REPLICA="$force_gke_functions_single_replica"
export SUPABASE_STORAGE_CLASS="$storage_class"
ensure_cross_cluster_db_host
helm_render_values
setup_knoe_db_for_supabase
local values="$PROJECT_ROOT/supabase/helm/generated/values.generated.json"
local db_frontdoor_manifest_path=""
local ns="supabase"
ns="$(load_manifest_summary_path supabase_namespace)"
ns="${ns:-supabase}"
db_frontdoor_manifest_path="$(load_manifest_summary_path manifests_frontdoor_db)"
if [[ "$HELM_TEMPLATE_ONLY" == "true" ]]; then
log "Helm template only; manifests ready in supabase/k8s"
return 0
fi
if [[ -z "${SUPABASE_PV_NODE:-}" && -n "${PROLE_CFG_PATH:-}" ]]; then
SUPABASE_PV_NODE=$(python3 -c "
import configparser
@ -2308,6 +2318,13 @@ print(base)" 2>/dev/null || true)
fi
local helm_set_args=()
if [[ "$force_gke_functions_single_replica" == "true" ]]; then
helm_set_args+=(
--set "deployment.functions.replicaCount=1"
--set "autoscaling.functions.enabled=false"
)
fi
if [[ -n "$storage_class" ]]; then
# Keep all non-DB Supabase PVC-backed components on the selected storage class.
helm_set_args+=(

View File

@ -18,6 +18,7 @@ import subprocess
import sys
import time
import urllib.parse
from copy import deepcopy
from pathlib import Path
from typing import Any, Dict
@ -215,6 +216,7 @@ def _split_frontdoor_docs(
"supabase-kong-declarative-config",
"supabase-kong-declarative-config-jwt",
}
functions_names = {"supabase-functions", "supabase-functions-config"}
frontdoor_docs: list[dict[str, Any]] = []
frontdoor_indexes: set[int] = set()
@ -228,35 +230,40 @@ def _split_frontdoor_docs(
is_studio = name in studio_names and kind in studio_kinds
is_kong = name in kong_names and kind in kong_kinds
if is_studio or is_kong:
frontdoor_indexes.add(idx)
is_functions = name in functions_names and kind in studio_kinds
if is_studio or is_kong or is_functions:
if is_studio or is_kong:
frontdoor_indexes.add(idx)
if kind not in publish_kinds:
continue
if not _is_valid_frontdoor_doc(doc):
continue
doc_for_frontdoor = deepcopy(doc)
meta_fd = doc_for_frontdoor.get("metadata") or {}
if frontdoor_release:
labels = meta.get("labels") if isinstance(meta, dict) else None
labels = meta_fd.get("labels") if isinstance(meta_fd, dict) else None
if not isinstance(labels, dict):
labels = {}
if isinstance(meta, dict):
meta["labels"] = labels
if isinstance(meta_fd, dict):
meta_fd["labels"] = labels
labels["app.kubernetes.io/instance"] = frontdoor_release
if kind == "Service":
spec = doc.get("spec")
spec = doc_for_frontdoor.get("spec")
if isinstance(spec, dict):
selector = spec.get("selector")
if isinstance(selector, dict):
selector["app.kubernetes.io/instance"] = frontdoor_release
if namespace:
if not isinstance(meta, dict):
meta = {}
doc["metadata"] = meta
if not str(meta.get("namespace") or "").strip():
meta["namespace"] = namespace
frontdoor_docs.append(doc)
if not isinstance(meta_fd, dict):
meta_fd = {}
doc_for_frontdoor["metadata"] = meta_fd
if not str(meta_fd.get("namespace") or "").strip():
meta_fd["namespace"] = namespace
frontdoor_docs.append(doc_for_frontdoor)
app_docs = [doc for idx, doc in enumerate(docs) if idx not in frontdoor_indexes]
return app_docs, frontdoor_docs
@ -859,13 +866,13 @@ def _build_overlay(cfg: configparser.ConfigParser, args: argparse.Namespace) ->
}
if supabase_storage_class:
# Validate: must be a synology/merlin mount — refuse pi/local SD card classes
# Validate: must be a synology/merlin/gke mount — refuse pi/local SD card classes
if not any(supabase_storage_class.startswith(p) for p in
("synology", "merlin-local-iscsi", "myrddin-local-iscsi")):
("synology", "merlin-local-iscsi", "myrddin-local-iscsi", "supabase-gke")):
raise SystemExit(
f"SUPABASE_STORAGE_CLASS '{supabase_storage_class}' is not a synology mount. "
"Supabase must run on iSCSI/NFS storage (merlin-local-iscsi-d002 or synology-iscsi). "
"Refusing deploy to prevent SD-card crash."
f"SUPABASE_STORAGE_CLASS '{supabase_storage_class}' is not a synology or GKE CSI mount. "
"Supabase must run on iSCSI/NFS or GKE CSI storage. "
"Refusing deploy to prevent node crash."
)
# Write to persistence.*.storageClassName — the path the Helm chart actually uses
persistence = overlay.setdefault("persistence", {})
@ -892,6 +899,15 @@ def _build_overlay(cfg: configparser.ConfigParser, args: argparse.Namespace) ->
component_cfg.setdefault("nodeSelector", {})
component_cfg["nodeSelector"].update(supabase_node_selector)
if _as_bool(os.environ.get("SUPABASE_GKE_FORCE_FUNCTIONS_SINGLE_REPLICA", "false")):
deployment = overlay.setdefault("deployment", {})
functions_cfg = deployment.setdefault("functions", {})
functions_cfg["replicaCount"] = 1
autoscaling = overlay.setdefault("autoscaling", {})
functions_as = autoscaling.setdefault("functions", {})
functions_as["enabled"] = False
meta = {
"supabase_namespace": supabase_ns,
"db_namespace": db_ns,

View File

@ -564,15 +564,30 @@ def test_supabase_deploy_db_frontdoor_readiness_and_storageclass_guards_present(
assert "Blocking Pods:" in script
def test_supabase_deploy_defaults_non_db_storage_class_to_standard_on_gke():
"""GKE Supabase non-DB PVC defaults must use pd-standard (`standard`) to avoid SSD/balanced quota pressure."""
def test_supabase_deploy_defaults_non_db_storage_class_to_csi_pd_standard_on_gke():
"""GKE Supabase non-DB PVC defaults must use a CSI pd-standard/WFFC class, not legacy `standard`."""
script = (REPO_ROOT / "supabase" / "deploy.sh").read_text(encoding="utf-8")
assert 'local sc_name="${SUPABASE_GKE_STORAGE_CLASS:-standard}"' in script
assert 'local preferred="${SUPABASE_DB_FRONTDOOR_STORAGE_CLASS:-standard}"' in script
assert 'local sc_name="${SUPABASE_GKE_STORAGE_CLASS:-supabase-gke-standard-rwo}"' in script
assert "provisioner: pd.csi.storage.gke.io" in script
assert "volumeBindingMode: WaitForFirstConsumer" in script
assert 'local preferred="${SUPABASE_DB_FRONTDOOR_STORAGE_CLASS:-$default_storage_class}"' in script
assert "Ignoring legacy 'standard' storage class for Supabase GKE non-DB PVCs" in script
assert 'preferred="standard"' in script
assert "SUPABASE_GKE_STORAGE_CLASS:-standard-rwo" not in script
assert "SUPABASE_DB_FRONTDOOR_STORAGE_CLASS:-standard-rwo" not in script
assert 'local sc_name="${SUPABASE_GKE_STORAGE_CLASS:-standard}"' not in script
assert 'local preferred="${SUPABASE_DB_FRONTDOOR_STORAGE_CLASS:-standard}"' not in script
def test_supabase_deploy_reconciles_gke_non_db_pvcs_and_forces_single_functions_replica():
"""GKE split deploy must reconcile mismatched APP PVC classes and force single-replica functions semantics."""
script = (REPO_ROOT / "supabase" / "deploy.sh").read_text(encoding="utf-8")
assert "reconcile_supabase_app_pvcs()" in script
assert "for pvc in supabase-deno supabase-functions supabase-imgproxy supabase-storage; do" in script
assert "Recreating APP PVC" in script
assert 'reconcile_supabase_app_pvcs "$ns" "$storage_class"' in script
assert 'SUPABASE_GKE_FORCE_FUNCTIONS_SINGLE_REPLICA="$force_gke_functions_single_replica"' in script
assert 'app_values.setdefault("replicaCount", {})["functions"] = 1' in script
def test_supabase_deploy_split_app_and_db_values_disable_general_node_role_enforcement():