feat: enhance configurable domain and ingress handling for GitLab and Supabase

- Introduced support for dynamic GitLab and Supabase domain and ingress configuration based on deployment mode (k8s vs local).
- Default GitLab domain and ingress class aligned with `prole.cfg` deployment settings.
- Expanded Supabase support:
  - Added configurable API and Studio hostnames with default fallbacks.
  - Introduced separate ingress class configuration for API and Studio.
  - Standardized public URL normalization for both services.
- Updated tests to validate domain, ingress, and configuration behaviors across environments.
This commit is contained in:
chrisfu 2026-04-11 16:01:02 -07:00
parent 091012a9e5
commit 6318d232f0
7 changed files with 258 additions and 65 deletions

View File

@ -48,13 +48,13 @@ env_setup.PROLE_DATA = $HOME/dev/prole/data
env_setup.PROLE_HOME = $HOME/dev/prole
env_setup.PROLE_LOGS = $HOME/dev/prole/logs
env_setup.PROLE_SERVICE = $HOME/dev/prole/etc
gitops.git_provider = Gitea
gitops.git_provider = GitLab
gitops.node_selector =
init_cluster.argocd_enabled = false
init_cluster.at_rest_encryption_enabled = true
init_cluster.cluster_env = prod
init_cluster.deployment_target = prole-prod-cluster
init_cluster.gitops_enabled = false
init_cluster.gitops_enabled = true
init_cluster.k3s_server_url = https://myrddin.prole.org:6443
init_cluster.k3s_token = ${PROLE_SECRET:v1:vxIxsLYZSClD_vzj:aSQOAaWbKOIT26PQPCqqIwenTzN4YKcaCa8OihlkDA9cdhbHrPeHtaeT3Pu140a67xKPV45ExSEiJ7jnWSW3lOH0KSO56qgzKxMXzrynKFfQVyJp5OzzQeCl-jrz15AdknLhPeh3eTmOEfLuhNY2Yy9fsBQ-l0gYikw37Q==}
init_cluster.kerberos_enabled = true
@ -103,12 +103,18 @@ DB_HOST_PORT = 5432
DB_PASSWORD= vzx.wC4Akd4x-Dj6Wguh
DEPLOYMENT_MODE = k8s
DEPLOYMENT_TARGET = prole-prod-cluster
GITLAB_DOMAIN = git.knoe.dev
GITLAB_INGRESS_CLASS = gce
DOCKER_PRELOAD = false
KNOE_DB_USER = root
KUBECONTEXT = gke_plenary-truck-485623-p7_us-west3_knoe-dev-0
OPTIONAL_WORKLOADS_MIN_READY_SCHEDULABLE_NODES = 2
PROLE_K3S_SERVER = https://myrddin.prole.org:6443
PROLE_K3S_TOKEN = ${PROLE_SECRET:v1:wEwOvfE5WeOdbVjW:yjYu4AJqgYo_EnwvYx4fgeSA1coQb8uv8FxE4_NNSIkQadVroCjaEpOyHXcStehLxGl7Bk-elU43yKzYQT_85BkLXp4-9k-LVh_kAfJp2cXlw7q8PczBr3-_MJXeWv1Cw7mCiMuWEfVpKjpCzZ5-URsBsIH3iYz20_6qwg==}
SUPABASE_API_HOSTNAME = api.knoe.dev
SUPABASE_HOSTNAME = db.0.knoe.dev
SUPABASE_INGRESS_CLASS = gce
SUPABASE_STUDIO_HOSTNAME = db.0.knoe.dev
SERVICE_NAMESPACE = knoe-system
SUPABASE_PV_BASE = /synology/d005
SUPABASE_PV_BASE_DIR = /synology/d005
@ -155,7 +161,7 @@ OLLAMA_SERVER_PORT = 11434
[Optional Features]
AT_REST_ENCRYPTION_ENABLED = True
GITOPS_ENABLED = False
GITOPS_ENABLED = True
KERBEROS_ENABLED = True
SUPABASE_ENABLED = True
@ -209,7 +215,7 @@ Configure routing, TLS, and frontdoor domain mappings
Prepare migration artifacts from source environment
Generate OpenTofu plan and apply production pipeline
MODE = k8s
OPENTOFU_VARS = {"admin_user":"knoe_admin","app_database":"knoey","app_user":"knoey_app","backup_bucket":"knoe-0-backups","backup_provider":"gcs","backup_retention_days":14,"bootstrap_admin_email":"admin@knoey.com","cluster_name":"knoe-prod","cnpg_cluster_name":"knoe-db","db_instances":3,"db_pgdata_size_gi":100,"db_pgdata_storage_class":"premium-rwo","db_storage_class":"premium-rwo","db_storage_size_gi":100,"db_wal_size_gi":25,"db_wal_storage_class":"premium-rwo","ecosystem_id":0,"environment":"production","frontdoor_host":"knoey.com","gcp_project_id":"plenary-truck-485623-p7","gcp_region":"us-west3","google_hosted_domain":"","meta_database":"knoe_meta","migration_continuous_until_cutover":false,"migration_mode":"snapshot-restore","migration_source_database":"knoey","migration_source_environment":"prole.org","migration_source_host":"knoe-local-db.prole.org","migration_source_password_ref":"secretref://local-source-db-password","migration_source_port":5432,"migration_source_user":"replication_user","namespace":"ecosystem-0","oidc_client_id_ref":"secretref://google-oidc-client-id","oidc_client_secret_ref":"secretref://google-oidc-client-secret","oidc_issuer":"https://accounts.google.com","platform_domain":"knoe.dev","postgres_version":"16","project_id":"plenary-truck-485623-p7","region":"us-west3","tls_mode":"managed","wal_bucket":"knoe-0-wal","workload_identity_sa":"cnpg-backup"}
OPENTOFU_VARS = {"admin_user":"knoe_admin","app_database":"knoey","app_user":"knoey_app","backup_bucket":"knoe-0-backups","backup_provider":"gcs","backup_retention_days":14,"bootstrap_admin_email":"admin@knoey.com","cluster_name":"knoe-prod","cnpg_cluster_name":"knoe-db","db_instances":3,"db_pgdata_size_gi":100,"db_pgdata_storage_class":"premium-rwo","db_storage_class":"premium-rwo","db_storage_size_gi":100,"db_wal_size_gi":25,"db_wal_storage_class":"premium-rwo","ecosystem_id":0,"environment":"production","frontdoor_host":"knoey.com","gcp_project_id":"plenary-truck-485623-p7","gcp_region":"us-west3","gitlab_host":"git.knoe.dev","google_hosted_domain":"","meta_database":"knoe_meta","migration_continuous_until_cutover":false,"migration_mode":"snapshot-restore","migration_source_database":"knoey","migration_source_environment":"prole.org","migration_source_host":"knoe-local-db.prole.org","migration_source_password_ref":"secretref://local-source-db-password","migration_source_port":5432,"migration_source_user":"replication_user","namespace":"ecosystem-0","oidc_client_id_ref":"secretref://google-oidc-client-id","oidc_client_secret_ref":"secretref://google-oidc-client-secret","oidc_issuer":"https://accounts.google.com","platform_domain":"knoe.dev","postgres_version":"16","project_id":"plenary-truck-485623-p7","region":"us-west3","supabase_api_host":"api.knoe.dev","supabase_studio_host":"db.0.knoe.dev","tls_mode":"managed","wal_bucket":"knoe-0-wal","workload_identity_sa":"cnpg-backup"}
PRODUCTION_CONFIG_YAML = kind: KnoeProductionConfig
metadata:
ecosystemId: 0
@ -260,6 +266,9 @@ spec:
routing:
frontdoorHost: knoey.com
platformDomain: knoe.dev
apiHost: api.knoe.dev
studioHost: db.0.knoe.dev
gitHost: git.knoe.dev
tlsMode: managed
migration:
sourceEnvironment: prole.org
@ -278,8 +287,8 @@ spec:
; No configuration values captured yet for this section.
[Deployment]
MODE = k3d
TARGET = prole-dev-cluster
MODE = k8s
TARGET = prole-prod-cluster
[Install]
; No configuration values captured yet for this section.

View File

@ -31,12 +31,12 @@ Options:
--help Show this help
Behavior:
- Removes any pre-existing git.prole.org Gitea configurations (helm release,
- Removes any pre-existing GitLab-domain Gitea configurations (helm release,
namespace, Kong routes) when --force is specified or when gitea is detected.
- Installs the GitLab Operator via Helm into the gitlab namespace.
- Provisions a GitLab CR that uses the knoe-db CloudNativePG cluster as its
external PostgreSQL data store.
- Configures git.prole.org as a Kong-managed ingress pointing to GitLab.
- Configures the GitLab public hostname as Kubernetes ingress pointing to GitLab.
- Pins all GitLab workloads to the selected node (default: gandalf.prole.org).
EOF
}
@ -116,8 +116,19 @@ REDIS_NAMESPACE="${REDIS_NAMESPACE:-${SERVICE_NAMESPACE:-knoe-system}}"
REDIS_HOST="${GITLAB_REDIS_HOST:-redis-master.${REDIS_NAMESPACE}.svc.cluster.local}"
REDIS_PORT="${GITLAB_REDIS_PORT:-6379}"
# GitLab domain (reuses the git.prole.org shared hostname)
GITLAB_DOMAIN="${GITLAB_DOMAIN:-git.prole.org}"
# GitLab public domain (configurable; default follows deployment mode)
_default_gitlab_domain="git.prole.org"
if [[ "$MODE" == "k8s" ]]; then
_default_gitlab_domain="git.knoe.dev"
fi
GITLAB_DOMAIN="${GITLAB_DOMAIN:-${GITLAB_HOSTNAME:-$_default_gitlab_domain}}"
# Public ingress class (k8s/GKE defaults to gce; local clusters keep kong)
_default_gitlab_ingress_class="kong"
if [[ "$MODE" == "k8s" ]]; then
_default_gitlab_ingress_class="gce"
fi
GITLAB_INGRESS_CLASS="${GITLAB_INGRESS_CLASS:-$_default_gitlab_ingress_class}"
# Google Workspace OIDC — FRONTDOOR_HOST gates OmniAuth configuration.
# On k3s: api.prole.org is the prole-auth SSO gateway (knoe-auth service).
@ -952,13 +963,13 @@ if [[ "${GITLAB_NO_WAIT:-0}" != "1" ]]; then
fi
# ---------------------------------------------------------------------------
# Kong ingress for git.prole.org -> gitlab-webservice
# The GitLab Operator creates the Ingress; this block patches it to Kong class
# or creates a fallback Kong-native ingress if the CR-managed one is absent.
# Public ingress for configured GitLab domain -> gitlab-webservice
# The GitLab Operator creates the Ingress; this block ensures a stable public
# class/host mapping if the CR-managed ingress is absent or not usable.
# ---------------------------------------------------------------------------
WEBSERVICE_SVC="${GITLAB_RELEASE}-webservice-default"
log "Ensuring Kong ingress for ${GITLAB_DOMAIN} -> ${WEBSERVICE_SVC}:8181 ..."
log "Ensuring ingress (${GITLAB_INGRESS_CLASS}) for ${GITLAB_DOMAIN} -> ${WEBSERVICE_SVC}:8181 ..."
kubectl apply -f - <<EOF
apiVersion: networking.k8s.io/v1
@ -967,10 +978,10 @@ metadata:
name: gitlab-kong-ingress
namespace: ${NAMESPACE}
annotations:
kubernetes.io/ingress.class: kong
kubernetes.io/ingress.class: ${GITLAB_INGRESS_CLASS}
konghq.com/strip-path: "false"
spec:
ingressClassName: kong
ingressClassName: ${GITLAB_INGRESS_CLASS}
rules:
- host: ${GITLAB_DOMAIN}
http:

View File

@ -526,6 +526,50 @@ if [[ -n "$_prole_cfg_file" ]]; then
export supabase_hostname="$_cfg_sbh"
export SUPABASE_HOSTNAME="$_cfg_sbh"
fi
_cfg_sapi=$(_prole_cfg_extract_key_in_files "SUPABASE_API_HOSTNAME" "${_prole_cfg_files[@]}")
if [[ -z "$_cfg_sapi" ]]; then
_cfg_sapi=$(_prole_cfg_extract_key_in_files "supabase_api_hostname" "${_prole_cfg_files[@]}")
fi
if [[ -n "$_cfg_sapi" ]]; then
export SUPABASE_API_HOSTNAME="$_cfg_sapi"
fi
_cfg_sstudio=$(_prole_cfg_extract_key_in_files "SUPABASE_STUDIO_HOSTNAME" "${_prole_cfg_files[@]}")
if [[ -z "$_cfg_sstudio" ]]; then
_cfg_sstudio=$(_prole_cfg_extract_key_in_files "supabase_studio_hostname" "${_prole_cfg_files[@]}")
fi
if [[ -n "$_cfg_sstudio" ]]; then
export SUPABASE_STUDIO_HOSTNAME="$_cfg_sstudio"
fi
_cfg_sing=$(_prole_cfg_extract_key_in_files "SUPABASE_INGRESS_CLASS" "${_prole_cfg_files[@]}")
if [[ -z "$_cfg_sing" ]]; then
_cfg_sing=$(_prole_cfg_extract_key_in_files "supabase_ingress_class" "${_prole_cfg_files[@]}")
fi
if [[ -n "$_cfg_sing" ]]; then
export SUPABASE_INGRESS_CLASS="$_cfg_sing"
fi
_cfg_gitdom=$(_prole_cfg_extract_key_in_files "GITLAB_DOMAIN" "${_prole_cfg_files[@]}")
if [[ -z "$_cfg_gitdom" ]]; then
_cfg_gitdom=$(_prole_cfg_extract_key_in_files "gitlab_domain" "${_prole_cfg_files[@]}")
fi
if [[ -z "$_cfg_gitdom" ]]; then
_cfg_gitdom=$(_prole_cfg_extract_key_in_files "GITLAB_HOSTNAME" "${_prole_cfg_files[@]}")
fi
if [[ -n "$_cfg_gitdom" ]]; then
export GITLAB_DOMAIN="$_cfg_gitdom"
fi
_cfg_ging=$(_prole_cfg_extract_key_in_files "GITLAB_INGRESS_CLASS" "${_prole_cfg_files[@]}")
if [[ -z "$_cfg_ging" ]]; then
_cfg_ging=$(_prole_cfg_extract_key_in_files "gitlab_ingress_class" "${_prole_cfg_files[@]}")
fi
if [[ -n "$_cfg_ging" ]]; then
export GITLAB_INGRESS_CLASS="$_cfg_ging"
fi
_cfg_ctx=$(_prole_cfg_extract_key_in_files "KUBE_CONTEXT_NAME" "${_prole_cfg_files[@]}")
if [[ -z "$_cfg_ctx" ]]; then
_cfg_ctx=$(_prole_cfg_extract_key_in_files "KUBECTL_CONTEXT" "${_prole_cfg_files[@]}")
@ -542,7 +586,7 @@ if [[ -n "$_prole_cfg_file" ]]; then
export KUBECONTEXT="$_cfg_ctx"
fi
fi
unset _cfg_ns _cfg_sns _cfg_dm _cfg_sh _cfg_sbh _cfg_ctx _prole_cfg_files _prole_cfg_f
unset _cfg_ns _cfg_sns _cfg_dm _cfg_sh _cfg_sbh _cfg_sapi _cfg_sstudio _cfg_sing _cfg_gitdom _cfg_ging _cfg_ctx _prole_cfg_files _prole_cfg_f
fi
if [[ -z "${PROLE_HOME:-}" && -d "$_prole_cfg_home_guess" ]]; then

View File

@ -73,6 +73,9 @@ class ProdAuthConfig:
class ProdRoutingConfig:
frontdoorHost: str = "knoey.com"
platformDomain: str = "knoe.dev"
apiHost: str = "api.knoe.dev"
studioHost: str = "db.0.knoe.dev"
gitHost: str = "git.knoe.dev"
tlsMode: str = "managed"
@ -258,6 +261,9 @@ def build_config(payload: dict[str, Any] | None = None) -> KnoeProductionConfig:
cfg.routing.frontdoorHost = _str_or_empty(routing.get("frontdoorHost")) or cfg.routing.frontdoorHost
cfg.routing.platformDomain = _str_or_empty(routing.get("platformDomain")) or cfg.routing.platformDomain
cfg.routing.apiHost = _str_or_empty(routing.get("apiHost")) or cfg.routing.apiHost
cfg.routing.studioHost = _str_or_empty(routing.get("studioHost")) or cfg.routing.studioHost
cfg.routing.gitHost = _str_or_empty(routing.get("gitHost")) or cfg.routing.gitHost
cfg.routing.tlsMode = _str_or_empty(routing.get("tlsMode")) or cfg.routing.tlsMode
cfg.migration.sourceEnvironment = (
@ -313,6 +319,9 @@ def validate_prod_config(config: KnoeProductionConfig) -> tuple[list[str], list[
"spec.auth.bootstrapAdminEmail": config.auth.bootstrapAdminEmail,
"spec.routing.frontdoorHost": config.routing.frontdoorHost,
"spec.routing.platformDomain": config.routing.platformDomain,
"spec.routing.apiHost": config.routing.apiHost,
"spec.routing.studioHost": config.routing.studioHost,
"spec.routing.gitHost": config.routing.gitHost,
"spec.migration.sourceEnvironment": config.migration.sourceEnvironment,
"spec.migration.sourceHost": config.migration.sourceHost,
"spec.migration.sourceDatabase": config.migration.sourceDatabase,
@ -348,6 +357,9 @@ def validate_prod_config(config: KnoeProductionConfig) -> tuple[list[str], list[
for field, value in (
("spec.routing.frontdoorHost", config.routing.frontdoorHost),
("spec.routing.platformDomain", config.routing.platformDomain),
("spec.routing.apiHost", config.routing.apiHost),
("spec.routing.studioHost", config.routing.studioHost),
("spec.routing.gitHost", config.routing.gitHost),
("spec.migration.sourceEnvironment", config.migration.sourceEnvironment),
):
if _str_or_empty(value) and not _is_domain(_str_or_empty(value)):
@ -410,6 +422,9 @@ def opentofu_vars(config: KnoeProductionConfig) -> dict[str, Any]:
"google_hosted_domain": config.auth.googleHostedDomain,
"frontdoor_host": config.routing.frontdoorHost,
"platform_domain": config.routing.platformDomain,
"supabase_api_host": config.routing.apiHost,
"supabase_studio_host": config.routing.studioHost,
"gitlab_host": config.routing.gitHost,
"tls_mode": config.routing.tlsMode,
"migration_source_environment": config.migration.sourceEnvironment,
"migration_mode": config.migration.mode,

View File

@ -2,7 +2,7 @@
"""
Render a personalized Supabase Helm values file and pre-template manifests
using the local prole.cfg. Defaults are tailored for knoe's CNPG cluster
(knoe-db-rw Service) and Traefik ingress.
(knoe-db-rw Service) and unattended k8s ingress.
"""
from __future__ import annotations
@ -106,6 +106,31 @@ def _parse_hosts(raw_hosts: str, fallback: str = "db.0.knoe.dev") -> list[str]:
return [fallback]
def _normalize_public_hosts(raw_hosts: str, fallback_host: str) -> tuple[list[str], str]:
raw_entries: list[str] = []
for entry in (raw_hosts or "").split(","):
token = (entry or "").strip()
if token and not _is_placeholder(token):
raw_entries.append(token)
if not raw_entries:
raw_entries = [fallback_host]
host_entries = _parse_hosts(",".join(raw_entries), fallback=fallback_host)
primary_host = host_entries[0]
primary_raw = (raw_entries[0] or "").strip()
if "://" in primary_raw:
parsed = urllib.parse.urlparse(primary_raw)
primary_host = (parsed.hostname or "").strip() or primary_host
scheme = (parsed.scheme or "https").strip()
netloc = (parsed.netloc or primary_host).strip()
public_url = f"{scheme}://{netloc}"
else:
public_url = f"https://{primary_host}"
return host_entries, public_url
def _discover_k8s_service_namespace(service_name: str) -> str:
try:
raw = subprocess.check_output(["kubectl", "get", "svc", "-A", "-o", "json"], text=True)
@ -208,9 +233,7 @@ def _build_overlay(cfg: configparser.ConfigParser, args: argparse.Namespace) ->
anon_key = _jwt(anon_payload, jwt_secret)
service_key = _jwt(service_payload, jwt_secret)
supabase_hostname_raw = _first(
os.environ.get("SUPABASE_STUDIO_URL", ""),
_cfg_get(cfg, "Inputs", "init_cluster.supabase_studio_url"),
legacy_supabase_hostname_raw = _first(
_cfg_get(cfg, "User", "supabase_hostname"),
_cfg_get(cfg, "Global", "supabase_hostname"),
_cfg_get(cfg, "User", "SUPABASE_HOSTNAME"),
@ -220,10 +243,38 @@ def _build_overlay(cfg: configparser.ConfigParser, args: argparse.Namespace) ->
default="db.0.knoe.dev",
).strip()
if _is_placeholder(supabase_hostname_raw):
supabase_hostname_raw = ""
if not supabase_hostname_raw:
supabase_hostname_raw = "db.0.knoe.dev"
if _is_placeholder(legacy_supabase_hostname_raw):
legacy_supabase_hostname_raw = ""
if not legacy_supabase_hostname_raw:
legacy_supabase_hostname_raw = "db.0.knoe.dev"
studio_hostname_raw = _first(
os.environ.get("SUPABASE_STUDIO_URL", ""),
_cfg_get(cfg, "Inputs", "init_cluster.supabase_studio_url"),
os.environ.get("SUPABASE_STUDIO_HOSTNAME", ""),
_cfg_get(cfg, "User", "SUPABASE_STUDIO_HOSTNAME"),
_cfg_get(cfg, "Global", "SUPABASE_STUDIO_HOSTNAME"),
legacy_supabase_hostname_raw,
default="db.0.knoe.dev",
).strip()
if _is_placeholder(studio_hostname_raw):
studio_hostname_raw = ""
if not studio_hostname_raw:
studio_hostname_raw = legacy_supabase_hostname_raw
api_hostname_raw = _first(
os.environ.get("SUPABASE_API_URL", ""),
_cfg_get(cfg, "Inputs", "init_cluster.supabase_api_url"),
os.environ.get("SUPABASE_API_HOSTNAME", ""),
_cfg_get(cfg, "User", "SUPABASE_API_HOSTNAME"),
_cfg_get(cfg, "Global", "SUPABASE_API_HOSTNAME"),
studio_hostname_raw,
default="api.knoe.dev",
).strip()
if _is_placeholder(api_hostname_raw):
api_hostname_raw = ""
if not api_hostname_raw:
api_hostname_raw = studio_hostname_raw
studio_enabled = _as_bool(
_first(
@ -271,30 +322,41 @@ def _build_overlay(cfg: configparser.ConfigParser, args: argparse.Namespace) ->
default=True,
)
raw_host_entries: list[str] = []
for entry in supabase_hostname_raw.split(","):
token = (entry or "").strip()
if token and not _is_placeholder(token):
raw_host_entries.append(token)
if not raw_host_entries:
raw_host_entries = ["db.0.knoe.dev"]
studio_host_entries, studio_public_url = _normalize_public_hosts(
studio_hostname_raw,
fallback_host="db.0.knoe.dev",
)
api_host_entries, api_public_url = _normalize_public_hosts(
api_hostname_raw,
fallback_host=studio_host_entries[0],
)
host_entries = _parse_hosts(",".join(raw_host_entries), fallback="db.0.knoe.dev")
ingress_host = host_entries[0]
# Canonical public URL for Supabase (used for auth callbacks + Studio links).
public_url = ""
primary_raw_host = (raw_host_entries[0] or "").strip()
if "://" in primary_raw_host:
parsed = urllib.parse.urlparse(primary_raw_host)
ingress_host = (parsed.hostname or "").strip() or ingress_host
scheme = (parsed.scheme or "https").strip()
if parsed.netloc:
public_url = f"{scheme}://{parsed.netloc}"
else:
public_url = f"{scheme}://{ingress_host}"
else:
public_url = f"https://{ingress_host}"
mode = _first(
os.environ.get("PROLE_MODE", ""),
_cfg_get(cfg, "Global", "DEPLOYMENT_MODE"),
_cfg_get(cfg, "globals", "prole.mode"),
default="",
).strip().lower()
default_ingress_class = "gce" if mode == "k8s" else "traefik"
api_ingress_class = _first(
os.environ.get("SUPABASE_API_INGRESS_CLASS", ""),
os.environ.get("SUPABASE_INGRESS_CLASS", ""),
_cfg_get(cfg, "User", "SUPABASE_API_INGRESS_CLASS"),
_cfg_get(cfg, "Global", "SUPABASE_API_INGRESS_CLASS"),
_cfg_get(cfg, "User", "SUPABASE_INGRESS_CLASS"),
_cfg_get(cfg, "Global", "SUPABASE_INGRESS_CLASS"),
default=default_ingress_class,
).strip()
if not api_ingress_class or _is_placeholder(api_ingress_class):
api_ingress_class = default_ingress_class
studio_ingress_class = _first(
os.environ.get("SUPABASE_STUDIO_INGRESS_CLASS", ""),
_cfg_get(cfg, "User", "SUPABASE_STUDIO_INGRESS_CLASS"),
_cfg_get(cfg, "Global", "SUPABASE_STUDIO_INGRESS_CLASS"),
api_ingress_class,
).strip()
if not studio_ingress_class or _is_placeholder(studio_ingress_class):
studio_ingress_class = api_ingress_class
# Additional origins/redirect URLs allowed by GoTrue (comma-separated hostnames or URLs).
# Needed when a service at a different hostname (e.g. svc.knoe.dev) uses Supabase auth.
@ -304,7 +366,13 @@ def _build_overlay(cfg: configparser.ConfigParser, args: argparse.Namespace) ->
_cfg_get(cfg, "Supabase", "ADDITIONAL_REDIRECT_URLS"),
default="",
).strip()
_extra_redirect_urls: list[str] = []
_allow_list_entries: list[str] = []
for _base_url in (studio_public_url, api_public_url):
if not _base_url:
continue
_candidate = _base_url.rstrip("/") + "/**"
if _candidate not in _allow_list_entries:
_allow_list_entries.append(_candidate)
for _entry in _extra_redirect_raw.split(","):
_entry = _entry.strip()
if not _entry or _is_placeholder(_entry):
@ -313,17 +381,11 @@ def _build_overlay(cfg: configparser.ConfigParser, args: argparse.Namespace) ->
_entry = f"https://{_entry}"
if not _entry.endswith("/**"):
_entry = _entry.rstrip("/") + "/**"
_extra_redirect_urls.append(_entry)
# Always include the primary public URL itself; only set if extra origins exist
_gotrue_uri_allow_list = ",".join([public_url + "/**"] + _extra_redirect_urls) if _extra_redirect_urls else ""
if _entry not in _allow_list_entries:
_allow_list_entries.append(_entry)
_gotrue_uri_allow_list = ",".join(_allow_list_entries)
# Placement: pin Supabase pods to the configured primary node (default: pi.knoe.dev)
mode = _first(
os.environ.get("PROLE_MODE", ""),
_cfg_get(cfg, "Global", "DEPLOYMENT_MODE"),
_cfg_get(cfg, "globals", "prole.mode"),
default="",
).strip().lower()
supabase_primary_node = _first(
os.environ.get("SUPABASE_PRIMARY_NODE", ""),
os.environ.get("SUPABASE_NODE_SELECTOR", ""),
@ -427,13 +489,13 @@ def _build_overlay(cfg: configparser.ConfigParser, args: argparse.Namespace) ->
"DB_HOST": db_host,
"DB_PORT": str(db_port),
"DB_SSL": "disable",
"API_EXTERNAL_URL": public_url,
"GOTRUE_SITE_URL": public_url,
"API_EXTERNAL_URL": api_public_url,
"GOTRUE_SITE_URL": studio_public_url,
**( {"GOTRUE_URI_ALLOW_LIST": _gotrue_uri_allow_list} if _gotrue_uri_allow_list else {} ),
},
"analytics": {"DB_HOST": db_host, "DB_PORT": str(db_port)},
"meta": {"DB_HOST": db_host, "DB_PORT": str(db_port)},
"studio": {"SUPABASE_PUBLIC_URL": public_url},
"studio": {"SUPABASE_PUBLIC_URL": studio_public_url},
},
"secret": {
"db": {"password": db_password, "database": db_name},
@ -442,13 +504,13 @@ def _build_overlay(cfg: configparser.ConfigParser, args: argparse.Namespace) ->
},
"ingress": {
"enabled": True,
"className": "traefik",
"hosts": [{"host": host, "paths": [{"path": "/", "pathType": "Prefix"}]} for host in host_entries],
"className": api_ingress_class,
"hosts": [{"host": host, "paths": [{"path": "/", "pathType": "Prefix"}]} for host in api_host_entries],
},
"studioIngress": {
"enabled": True,
"className": "traefik",
"hosts": [{"host": host, "paths": [{"path": "/", "pathType": "Prefix"}]} for host in host_entries],
"className": studio_ingress_class,
"hosts": [{"host": host, "paths": [{"path": "/", "pathType": "Prefix"}]} for host in studio_host_entries],
},
}

View File

@ -32,6 +32,9 @@ def test_default_prod_config_uses_knoey_knoe_conventions():
assert cfg.backups.walBucket == "knoe-0-wal"
assert cfg.routing.frontdoorHost == "knoey.com"
assert cfg.routing.platformDomain == "knoe.dev"
assert cfg.routing.apiHost == "api.knoe.dev"
assert cfg.routing.studioHost == "db.0.knoe.dev"
assert cfg.routing.gitHost == "git.knoe.dev"
assert cfg.migration.sourceEnvironment == "prole.org"
@ -44,6 +47,7 @@ def test_validation_requires_project_and_enforces_ecosystem_and_positive_numbers
cfg.backups.retentionDays = 0
cfg.migration.sourcePort = 0
cfg.routing.frontdoorHost = "not_a_domain"
cfg.routing.apiHost = "not_a_domain"
errors, _warnings = validate_prod_config(cfg)
@ -54,6 +58,7 @@ def test_validation_requires_project_and_enforces_ecosystem_and_positive_numbers
assert "spec.backups.retentionDays must be a positive number" in errors
assert "spec.migration.sourcePort must be a positive number" in errors
assert "spec.routing.frontdoorHost must be a valid domain" in errors
assert "spec.routing.apiHost must be a valid domain" in errors
def test_yaml_and_opentofu_preview_contains_expected_values():
@ -73,6 +78,9 @@ def test_yaml_and_opentofu_preview_contains_expected_values():
assert "pgdataStorageClass: standard-rwo" in yaml_preview
assert "walStorageClass: premium-rwo" in yaml_preview
assert "frontdoorHost: knoey.com" in yaml_preview
assert "apiHost: api.knoe.dev" in yaml_preview
assert "studioHost: db.0.knoe.dev" in yaml_preview
assert "gitHost: git.knoe.dev" in yaml_preview
assert tf_vars["project_id"] == "my-gcp-project"
assert tf_vars["namespace"] == "ecosystem-0"
assert tf_vars["cnpg_cluster_name"] == "knoe-db"
@ -80,6 +88,9 @@ def test_yaml_and_opentofu_preview_contains_expected_values():
assert tf_vars["db_wal_storage_class"] == "premium-rwo"
assert tf_vars["db_pgdata_size_gi"] == 120
assert tf_vars["db_wal_size_gi"] == 32
assert tf_vars["supabase_api_host"] == "api.knoe.dev"
assert tf_vars["supabase_studio_host"] == "db.0.knoe.dev"
assert tf_vars["gitlab_host"] == "git.knoe.dev"
def test_prod_config_api_plan_apply_status_and_logs_flow():

View File

@ -45,6 +45,7 @@ def test_render_supabase_uses_supabase_hostname_for_public_urls(tmp_path: Path,
[Global]
NAMESPACE = test-ns
STORAGE_BACKEND = local
""",
)
@ -77,6 +78,7 @@ def test_render_supabase_allows_scheme_in_supabase_hostname(tmp_path: Path, monk
[Global]
NAMESPACE = test-ns
STORAGE_BACKEND = local
""",
)
@ -87,3 +89,42 @@ def test_render_supabase_allows_scheme_in_supabase_hostname(tmp_path: Path, monk
assert overlay["ingress"]["hosts"][0]["host"] == "db.prole.org"
assert overlay["environment"]["auth"]["API_EXTERNAL_URL"] == "http://db.prole.org"
assert overlay["environment"]["studio"]["SUPABASE_PUBLIC_URL"] == "http://db.prole.org"
def test_render_supabase_splits_api_and_studio_hosts_for_k8s(tmp_path: Path, monkeypatch):
monkeypatch.delenv("SUPABASE_HOST", raising=False)
monkeypatch.delenv("SUPABASE_HOSTNAME", raising=False)
monkeypatch.delenv("KNOE_DB_NAMESPACE", raising=False)
monkeypatch.delenv("KNOE_DB_SERVICE", raising=False)
cfg_path = tmp_path / "prole.cfg"
_write_cfg(
cfg_path,
"""
[Inputs]
init_password.db_password = test-password
init_password.db_host_port = 5432
[Global]
NAMESPACE = test-ns
DEPLOYMENT_MODE = k8s
SUPABASE_API_HOSTNAME = api.knoe.dev
SUPABASE_STUDIO_HOSTNAME = db.0.knoe.dev
STORAGE_BACKEND = local
""",
)
cfg = _read_cfg(cfg_path)
args = Namespace(output_dir=str(tmp_path / "gen"), manifests_dir=str(tmp_path / "k8s"))
overlay, _meta = _build_overlay(cfg, args)
assert overlay["ingress"]["hosts"][0]["host"] == "api.knoe.dev"
assert overlay["studioIngress"]["hosts"][0]["host"] == "db.0.knoe.dev"
assert overlay["ingress"]["className"] == "gce"
assert overlay["studioIngress"]["className"] == "gce"
assert overlay["environment"]["auth"]["API_EXTERNAL_URL"] == "https://api.knoe.dev"
assert overlay["environment"]["auth"]["GOTRUE_SITE_URL"] == "https://db.0.knoe.dev"
assert overlay["environment"]["studio"]["SUPABASE_PUBLIC_URL"] == "https://db.0.knoe.dev"
assert overlay["environment"]["auth"]["GOTRUE_URI_ALLOW_LIST"] == (
"https://db.0.knoe.dev/**,https://api.knoe.dev/**"
)