mirror of
https://github.com/dredx/prole.git
synced 2026-09-24 15:44:33 +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)
|
clear_stale_app_scheduling(app_values)
|
||||||
|
|
||||||
db_values = deepcopy(base)
|
db_values = deepcopy(base)
|
||||||
|
clear_stale_app_scheduling(db_values)
|
||||||
for component in (
|
for component in (
|
||||||
"analytics",
|
"analytics",
|
||||||
"auth",
|
"auth",
|
||||||
"functions",
|
|
||||||
"imgproxy",
|
"imgproxy",
|
||||||
"meta",
|
"meta",
|
||||||
"minio",
|
"minio",
|
||||||
@ -130,7 +130,13 @@ force_single_functions_replica = os.environ.get(
|
|||||||
"SUPABASE_GKE_FORCE_FUNCTIONS_SINGLE_REPLICA", ""
|
"SUPABASE_GKE_FORCE_FUNCTIONS_SINGLE_REPLICA", ""
|
||||||
).strip().lower() in {"1", "true", "yes", "on"}
|
).strip().lower() in {"1", "true", "yes", "on"}
|
||||||
if force_single_functions_replica:
|
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(app_values, open(app_path, "w", encoding="utf-8"), indent=2)
|
||||||
json.dump(db_values, open(db_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"
|
local db_frontdoor_release="${HELM_RELEASE}-frontdoor-db"
|
||||||
export SUPABASE_FRONTDOOR_DB_RELEASE="$db_frontdoor_release"
|
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
|
local storage_class
|
||||||
storage_class="${SUPABASE_STORAGE_CLASS:-}"
|
storage_class="${SUPABASE_STORAGE_CLASS:-}"
|
||||||
local first_node_name=""
|
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"
|
force_gke_functions_single_replica="true"
|
||||||
fi
|
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
|
if [[ -z "${SUPABASE_PV_NODE:-}" && -n "${PROLE_CFG_PATH:-}" ]]; then
|
||||||
SUPABASE_PV_NODE=$(python3 -c "
|
SUPABASE_PV_NODE=$(python3 -c "
|
||||||
import configparser
|
import configparser
|
||||||
@ -2308,6 +2318,13 @@ print(base)" 2>/dev/null || true)
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
local helm_set_args=()
|
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
|
if [[ -n "$storage_class" ]]; then
|
||||||
# Keep all non-DB Supabase PVC-backed components on the selected storage class.
|
# Keep all non-DB Supabase PVC-backed components on the selected storage class.
|
||||||
helm_set_args+=(
|
helm_set_args+=(
|
||||||
|
|||||||
@ -18,6 +18,7 @@ import subprocess
|
|||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
import urllib.parse
|
import urllib.parse
|
||||||
|
from copy import deepcopy
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Dict
|
from typing import Any, Dict
|
||||||
|
|
||||||
@ -215,6 +216,7 @@ def _split_frontdoor_docs(
|
|||||||
"supabase-kong-declarative-config",
|
"supabase-kong-declarative-config",
|
||||||
"supabase-kong-declarative-config-jwt",
|
"supabase-kong-declarative-config-jwt",
|
||||||
}
|
}
|
||||||
|
functions_names = {"supabase-functions", "supabase-functions-config"}
|
||||||
|
|
||||||
frontdoor_docs: list[dict[str, Any]] = []
|
frontdoor_docs: list[dict[str, Any]] = []
|
||||||
frontdoor_indexes: set[int] = set()
|
frontdoor_indexes: set[int] = set()
|
||||||
@ -228,6 +230,8 @@ def _split_frontdoor_docs(
|
|||||||
|
|
||||||
is_studio = name in studio_names and kind in studio_kinds
|
is_studio = name in studio_names and kind in studio_kinds
|
||||||
is_kong = name in kong_names and kind in kong_kinds
|
is_kong = name in kong_names and kind in kong_kinds
|
||||||
|
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:
|
if is_studio or is_kong:
|
||||||
frontdoor_indexes.add(idx)
|
frontdoor_indexes.add(idx)
|
||||||
if kind not in publish_kinds:
|
if kind not in publish_kinds:
|
||||||
@ -235,28 +239,31 @@ def _split_frontdoor_docs(
|
|||||||
if not _is_valid_frontdoor_doc(doc):
|
if not _is_valid_frontdoor_doc(doc):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
doc_for_frontdoor = deepcopy(doc)
|
||||||
|
meta_fd = doc_for_frontdoor.get("metadata") or {}
|
||||||
|
|
||||||
if frontdoor_release:
|
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):
|
if not isinstance(labels, dict):
|
||||||
labels = {}
|
labels = {}
|
||||||
if isinstance(meta, dict):
|
if isinstance(meta_fd, dict):
|
||||||
meta["labels"] = labels
|
meta_fd["labels"] = labels
|
||||||
labels["app.kubernetes.io/instance"] = frontdoor_release
|
labels["app.kubernetes.io/instance"] = frontdoor_release
|
||||||
|
|
||||||
if kind == "Service":
|
if kind == "Service":
|
||||||
spec = doc.get("spec")
|
spec = doc_for_frontdoor.get("spec")
|
||||||
if isinstance(spec, dict):
|
if isinstance(spec, dict):
|
||||||
selector = spec.get("selector")
|
selector = spec.get("selector")
|
||||||
if isinstance(selector, dict):
|
if isinstance(selector, dict):
|
||||||
selector["app.kubernetes.io/instance"] = frontdoor_release
|
selector["app.kubernetes.io/instance"] = frontdoor_release
|
||||||
|
|
||||||
if namespace:
|
if namespace:
|
||||||
if not isinstance(meta, dict):
|
if not isinstance(meta_fd, dict):
|
||||||
meta = {}
|
meta_fd = {}
|
||||||
doc["metadata"] = meta
|
doc_for_frontdoor["metadata"] = meta_fd
|
||||||
if not str(meta.get("namespace") or "").strip():
|
if not str(meta_fd.get("namespace") or "").strip():
|
||||||
meta["namespace"] = namespace
|
meta_fd["namespace"] = namespace
|
||||||
frontdoor_docs.append(doc)
|
frontdoor_docs.append(doc_for_frontdoor)
|
||||||
|
|
||||||
app_docs = [doc for idx, doc in enumerate(docs) if idx not in frontdoor_indexes]
|
app_docs = [doc for idx, doc in enumerate(docs) if idx not in frontdoor_indexes]
|
||||||
return app_docs, frontdoor_docs
|
return app_docs, frontdoor_docs
|
||||||
@ -859,13 +866,13 @@ def _build_overlay(cfg: configparser.ConfigParser, args: argparse.Namespace) ->
|
|||||||
}
|
}
|
||||||
|
|
||||||
if supabase_storage_class:
|
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
|
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(
|
raise SystemExit(
|
||||||
f"SUPABASE_STORAGE_CLASS '{supabase_storage_class}' is not a synology mount. "
|
f"SUPABASE_STORAGE_CLASS '{supabase_storage_class}' is not a synology or GKE CSI mount. "
|
||||||
"Supabase must run on iSCSI/NFS storage (merlin-local-iscsi-d002 or synology-iscsi). "
|
"Supabase must run on iSCSI/NFS or GKE CSI storage. "
|
||||||
"Refusing deploy to prevent SD-card crash."
|
"Refusing deploy to prevent node crash."
|
||||||
)
|
)
|
||||||
# Write to persistence.*.storageClassName — the path the Helm chart actually uses
|
# Write to persistence.*.storageClassName — the path the Helm chart actually uses
|
||||||
persistence = overlay.setdefault("persistence", {})
|
persistence = overlay.setdefault("persistence", {})
|
||||||
@ -892,6 +899,15 @@ def _build_overlay(cfg: configparser.ConfigParser, args: argparse.Namespace) ->
|
|||||||
component_cfg.setdefault("nodeSelector", {})
|
component_cfg.setdefault("nodeSelector", {})
|
||||||
component_cfg["nodeSelector"].update(supabase_node_selector)
|
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 = {
|
meta = {
|
||||||
"supabase_namespace": supabase_ns,
|
"supabase_namespace": supabase_ns,
|
||||||
"db_namespace": db_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
|
assert "Blocking Pods:" in script
|
||||||
|
|
||||||
|
|
||||||
def test_supabase_deploy_defaults_non_db_storage_class_to_standard_on_gke():
|
def test_supabase_deploy_defaults_non_db_storage_class_to_csi_pd_standard_on_gke():
|
||||||
"""GKE Supabase non-DB PVC defaults must use pd-standard (`standard`) to avoid SSD/balanced quota pressure."""
|
"""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")
|
script = (REPO_ROOT / "supabase" / "deploy.sh").read_text(encoding="utf-8")
|
||||||
|
|
||||||
assert 'local sc_name="${SUPABASE_GKE_STORAGE_CLASS:-standard}"' in script
|
assert 'local sc_name="${SUPABASE_GKE_STORAGE_CLASS:-supabase-gke-standard-rwo}"' in script
|
||||||
assert 'local preferred="${SUPABASE_DB_FRONTDOOR_STORAGE_CLASS:-standard}"' 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 'preferred="standard"' in script
|
||||||
assert "SUPABASE_GKE_STORAGE_CLASS:-standard-rwo" not in script
|
assert 'local sc_name="${SUPABASE_GKE_STORAGE_CLASS:-standard}"' not in script
|
||||||
assert "SUPABASE_DB_FRONTDOOR_STORAGE_CLASS:-standard-rwo" 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():
|
def test_supabase_deploy_split_app_and_db_values_disable_general_node_role_enforcement():
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user