mirror of
https://github.com/dredx/prole.git
synced 2026-09-27 19:34:30 +00:00
- Makefile: Added 'init' and 'deploy' targets for k3s parity and Gitea staging. - OpenTofu: Fixed namespace handling in k3s main.tf to prevent metadata overwrites. - UI: Added 'GitOps' and 'Database Options' configuration screens. - Core: Enhanced monitoring, milestones, and environment handling for new services. - Supabase: Integrated full Helm chart and manifest rendering logic. - Gitea: Added deployment scripts and GitOps sync support. - Database: Added Percona/Postgres Dockerfile templates and improved TDE scripts. - Tests: Added coverage for new UI screens and navigation flows.
231 lines
7.3 KiB
Python
Executable File
231 lines
7.3 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""
|
|
Render a personalized Supabase Helm values file and pre-template manifests
|
|
using the local prole.cfg. Defaults are tailored for Prole's CNPG cluster
|
|
(prole-db-rw Service) and Traefik ingress.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import base64
|
|
import configparser
|
|
import hashlib
|
|
import hmac
|
|
import json
|
|
import os
|
|
import secrets
|
|
import subprocess
|
|
import sys
|
|
import time
|
|
from pathlib import Path
|
|
from typing import Any, Dict
|
|
|
|
# Allow imports from the repo root
|
|
REPO_ROOT = Path(__file__).resolve().parents[2]
|
|
sys.path.insert(0, str(REPO_ROOT))
|
|
|
|
try:
|
|
from installer.config import _resolve_secret_value # type: ignore
|
|
except Exception: # pragma: no cover - fallback for minimal environments
|
|
def _resolve_secret_value(value: str | None) -> str:
|
|
return value or ""
|
|
|
|
|
|
def _read_cfg(path: Path) -> configparser.ConfigParser:
|
|
parser = configparser.ConfigParser(interpolation=None)
|
|
parser.read(path)
|
|
return parser
|
|
|
|
|
|
def _cfg_get(cfg: configparser.ConfigParser, section: str, key: str, default: str = "") -> str:
|
|
try:
|
|
raw = cfg.get(section, key, fallback=default)
|
|
except Exception:
|
|
return default
|
|
return _resolve_secret_value((raw or "").strip())
|
|
|
|
|
|
def _first(*values: str, default: str = "") -> str:
|
|
for v in values:
|
|
if v:
|
|
return v
|
|
return default
|
|
|
|
|
|
def _ensure_dir(path: Path) -> Path:
|
|
path.mkdir(parents=True, exist_ok=True)
|
|
return path
|
|
|
|
|
|
def _load_or_create_secret(path: Path, length: int = 32) -> str:
|
|
if path.exists():
|
|
return path.read_text().strip()
|
|
val = secrets.token_hex(length)
|
|
path.write_text(val)
|
|
return val
|
|
|
|
|
|
def _b64url(data: bytes) -> str:
|
|
return base64.urlsafe_b64encode(data).rstrip(b"=").decode("utf-8")
|
|
|
|
|
|
def _jwt(payload: Dict[str, Any], secret: str) -> str:
|
|
header = {"alg": "HS256", "typ": "JWT"}
|
|
head = _b64url(json.dumps(header, separators=(",", ":")).encode())
|
|
body = _b64url(json.dumps(payload, separators=(",", ":")).encode())
|
|
signing_input = f"{head}.{body}".encode()
|
|
sig = hmac.new(secret.encode(), signing_input, hashlib.sha256).digest()
|
|
return f"{head}.{body}.{_b64url(sig)}"
|
|
|
|
|
|
def _build_overlay(cfg: configparser.ConfigParser, args: argparse.Namespace) -> tuple[dict, dict]:
|
|
db_ns = _first(
|
|
_cfg_get(cfg, "Global", "NAMESPACE"),
|
|
os.environ.get("NAMESPACE", ""),
|
|
"default",
|
|
)
|
|
supabase_ns = _first(
|
|
_cfg_get(cfg, "Supabase", "NAMESPACE"),
|
|
os.environ.get("SUPABASE_NAMESPACE", ""),
|
|
"supabase",
|
|
)
|
|
db_service = os.environ.get("PROLE_DB_SERVICE", "prole-db-rw")
|
|
db_host = f"{db_service}.{db_ns}.svc.cluster.local"
|
|
db_port = _first(
|
|
_cfg_get(cfg, "Global", "DB_HOST_PORT"),
|
|
_cfg_get(cfg, "Inputs", "init_password.db_host_port"),
|
|
"5432",
|
|
)
|
|
db_name = _first(
|
|
_cfg_get(cfg, "Database Creation", "DB_NAME"),
|
|
_cfg_get(cfg, "Global", "DB_NAME"),
|
|
"postgres",
|
|
)
|
|
db_password = _first(
|
|
_cfg_get(cfg, "Inputs", "init_password.db_password"),
|
|
_cfg_get(cfg, "Global", "DB_PASSWORD"),
|
|
os.environ.get("DB_PASSWORD", ""),
|
|
)
|
|
if not db_password:
|
|
raise SystemExit("Database password is required (init_password.db_password or DB_PASSWORD).")
|
|
|
|
gen_dir = _ensure_dir(Path(args.output_dir))
|
|
secrets_dir = _ensure_dir(gen_dir / "secrets")
|
|
|
|
jwt_secret = _first(os.environ.get("SUPABASE_JWT_SECRET", "")) or _load_or_create_secret(
|
|
secrets_dir / "jwt.secret", length=32
|
|
)
|
|
now = int(time.time())
|
|
exp = now + 10 * 365 * 24 * 3600
|
|
anon_payload = {"role": "anon", "iss": "prole-supabase", "iat": now, "exp": exp}
|
|
service_payload = {"role": "service_role", "iss": "prole-supabase", "iat": now, "exp": exp}
|
|
anon_key = _jwt(anon_payload, jwt_secret)
|
|
service_key = _jwt(service_payload, jwt_secret)
|
|
|
|
ingress_host = _first(os.environ.get("SUPABASE_HOST", ""), "supabase.prole.org")
|
|
|
|
overlay: dict[str, Any] = {
|
|
"nameOverride": "supabase",
|
|
"fullnameOverride": "supabase",
|
|
"deployment": {
|
|
"db": {"enabled": False},
|
|
"functions": {"enabled": False},
|
|
"vector": {"enabled": False},
|
|
},
|
|
"environment": {
|
|
"auth": {"DB_HOST": db_host, "DB_PORT": str(db_port), "DB_SSL": "disable"},
|
|
"analytics": {"DB_HOST": db_host, "DB_PORT": str(db_port)},
|
|
"meta": {"DB_HOST": db_host, "DB_PORT": str(db_port)},
|
|
},
|
|
"secret": {
|
|
"db": {"password": db_password, "database": db_name},
|
|
"jwt": {"secret": jwt_secret, "anonKey": anon_key, "serviceKey": service_key},
|
|
},
|
|
"ingress": {
|
|
"enabled": True,
|
|
"className": "traefik",
|
|
"hosts": [
|
|
{"host": ingress_host, "paths": [{"path": "/", "pathType": "Prefix"}]}
|
|
],
|
|
},
|
|
}
|
|
|
|
meta = {
|
|
"supabase_namespace": supabase_ns,
|
|
"db_namespace": db_ns,
|
|
"db_host": db_host,
|
|
}
|
|
return overlay, meta
|
|
|
|
|
|
def render(args: argparse.Namespace) -> None:
|
|
cfg_path = Path(args.config)
|
|
if cfg_path.is_dir():
|
|
cfg_path = cfg_path / "prole.cfg"
|
|
if not cfg_path.exists():
|
|
raise SystemExit(f"Config not found: {cfg_path}")
|
|
|
|
cfg = _read_cfg(cfg_path)
|
|
overlay, meta = _build_overlay(cfg, args)
|
|
|
|
gen_dir = _ensure_dir(Path(args.output_dir))
|
|
values_path = gen_dir / "values.generated.json"
|
|
values_path.write_text(json.dumps(overlay, indent=2))
|
|
|
|
# Write Namespace manifest for Supabase
|
|
k8s_dir = _ensure_dir(Path(args.manifests_dir))
|
|
namespace_manifest = {
|
|
"apiVersion": "v1",
|
|
"kind": "Namespace",
|
|
"metadata": {"name": meta["supabase_namespace"]},
|
|
}
|
|
(k8s_dir / "namespace.yaml").write_text(json.dumps(namespace_manifest))
|
|
|
|
# Render chart to a single manifest file (Helm accepts JSON values)
|
|
rendered_path = k8s_dir / "supabase-helm.yaml"
|
|
cmd = [
|
|
"helm",
|
|
"template",
|
|
"prole-supabase",
|
|
str(REPO_ROOT / "supabase" / "helm" / "prole-supabase"),
|
|
"-n",
|
|
meta["supabase_namespace"],
|
|
"-f",
|
|
str(values_path),
|
|
]
|
|
proc = subprocess.run(cmd, capture_output=True, text=True)
|
|
if proc.returncode != 0:
|
|
sys.stderr.write(proc.stderr)
|
|
raise SystemExit(f"Helm template failed (code {proc.returncode})")
|
|
rendered_path.write_text(proc.stdout)
|
|
|
|
summary = {
|
|
"values": str(values_path),
|
|
"manifests": str(rendered_path),
|
|
"db_host": meta["db_host"],
|
|
"supabase_namespace": meta["supabase_namespace"],
|
|
}
|
|
(gen_dir / "manifest-summary.json").write_text(json.dumps(summary, indent=2))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
parser = argparse.ArgumentParser(description="Render Supabase Helm chart from prole.cfg")
|
|
parser.add_argument(
|
|
"--config",
|
|
"-c",
|
|
default=os.environ.get("PROLE_CONF", str(REPO_ROOT / "conf" / "prole.cfg")),
|
|
help="Path to prole.cfg or its directory",
|
|
)
|
|
parser.add_argument(
|
|
"--output-dir",
|
|
default=str(REPO_ROOT / "supabase" / "helm" / "generated"),
|
|
help="Directory to write generated values and metadata",
|
|
)
|
|
parser.add_argument(
|
|
"--manifests-dir",
|
|
default=str(REPO_ROOT / "supabase" / "k8s"),
|
|
help="Directory to write rendered Kubernetes manifests",
|
|
)
|
|
args = parser.parse_args()
|
|
render(args)
|