mirror of
https://github.com/dredx/prole.git
synced 2026-09-23 11:03:59 +00:00
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:
parent
fddaf80823
commit
174877201e
@ -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+=(
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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():
|
||||
|
||||
Loading…
Reference in New Issue
Block a user