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_HOME = $HOME/dev/prole
env_setup.PROLE_LOGS = $HOME/dev/prole/logs env_setup.PROLE_LOGS = $HOME/dev/prole/logs
env_setup.PROLE_SERVICE = $HOME/dev/prole/etc env_setup.PROLE_SERVICE = $HOME/dev/prole/etc
gitops.git_provider = Gitea gitops.git_provider = GitLab
gitops.node_selector = gitops.node_selector =
init_cluster.argocd_enabled = false init_cluster.argocd_enabled = false
init_cluster.at_rest_encryption_enabled = true init_cluster.at_rest_encryption_enabled = true
init_cluster.cluster_env = prod init_cluster.cluster_env = prod
init_cluster.deployment_target = prole-prod-cluster 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_server_url = https://myrddin.prole.org:6443
init_cluster.k3s_token = ${PROLE_SECRET:v1:vxIxsLYZSClD_vzj:aSQOAaWbKOIT26PQPCqqIwenTzN4YKcaCa8OihlkDA9cdhbHrPeHtaeT3Pu140a67xKPV45ExSEiJ7jnWSW3lOH0KSO56qgzKxMXzrynKFfQVyJp5OzzQeCl-jrz15AdknLhPeh3eTmOEfLuhNY2Yy9fsBQ-l0gYikw37Q==} init_cluster.k3s_token = ${PROLE_SECRET:v1:vxIxsLYZSClD_vzj:aSQOAaWbKOIT26PQPCqqIwenTzN4YKcaCa8OihlkDA9cdhbHrPeHtaeT3Pu140a67xKPV45ExSEiJ7jnWSW3lOH0KSO56qgzKxMXzrynKFfQVyJp5OzzQeCl-jrz15AdknLhPeh3eTmOEfLuhNY2Yy9fsBQ-l0gYikw37Q==}
init_cluster.kerberos_enabled = true init_cluster.kerberos_enabled = true
@ -103,12 +103,18 @@ DB_HOST_PORT = 5432
DB_PASSWORD= vzx.wC4Akd4x-Dj6Wguh DB_PASSWORD= vzx.wC4Akd4x-Dj6Wguh
DEPLOYMENT_MODE = k8s DEPLOYMENT_MODE = k8s
DEPLOYMENT_TARGET = prole-prod-cluster DEPLOYMENT_TARGET = prole-prod-cluster
GITLAB_DOMAIN = git.knoe.dev
GITLAB_INGRESS_CLASS = gce
DOCKER_PRELOAD = false DOCKER_PRELOAD = false
KNOE_DB_USER = root KNOE_DB_USER = root
KUBECONTEXT = gke_plenary-truck-485623-p7_us-west3_knoe-dev-0 KUBECONTEXT = gke_plenary-truck-485623-p7_us-west3_knoe-dev-0
OPTIONAL_WORKLOADS_MIN_READY_SCHEDULABLE_NODES = 2 OPTIONAL_WORKLOADS_MIN_READY_SCHEDULABLE_NODES = 2
PROLE_K3S_SERVER = https://myrddin.prole.org:6443 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==} 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 SERVICE_NAMESPACE = knoe-system
SUPABASE_PV_BASE = /synology/d005 SUPABASE_PV_BASE = /synology/d005
SUPABASE_PV_BASE_DIR = /synology/d005 SUPABASE_PV_BASE_DIR = /synology/d005
@ -155,7 +161,7 @@ OLLAMA_SERVER_PORT = 11434
[Optional Features] [Optional Features]
AT_REST_ENCRYPTION_ENABLED = True AT_REST_ENCRYPTION_ENABLED = True
GITOPS_ENABLED = False GITOPS_ENABLED = True
KERBEROS_ENABLED = True KERBEROS_ENABLED = True
SUPABASE_ENABLED = True SUPABASE_ENABLED = True
@ -209,7 +215,7 @@ Configure routing, TLS, and frontdoor domain mappings
Prepare migration artifacts from source environment Prepare migration artifacts from source environment
Generate OpenTofu plan and apply production pipeline Generate OpenTofu plan and apply production pipeline
MODE = k8s 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 PRODUCTION_CONFIG_YAML = kind: KnoeProductionConfig
metadata: metadata:
ecosystemId: 0 ecosystemId: 0
@ -260,6 +266,9 @@ spec:
routing: routing:
frontdoorHost: knoey.com frontdoorHost: knoey.com
platformDomain: knoe.dev platformDomain: knoe.dev
apiHost: api.knoe.dev
studioHost: db.0.knoe.dev
gitHost: git.knoe.dev
tlsMode: managed tlsMode: managed
migration: migration:
sourceEnvironment: prole.org sourceEnvironment: prole.org
@ -278,8 +287,8 @@ spec:
; No configuration values captured yet for this section. ; No configuration values captured yet for this section.
[Deployment] [Deployment]
MODE = k3d MODE = k8s
TARGET = prole-dev-cluster TARGET = prole-prod-cluster
[Install] [Install]
; No configuration values captured yet for this section. ; No configuration values captured yet for this section.

View File

@ -31,12 +31,12 @@ Options:
--help Show this help --help Show this help
Behavior: 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. namespace, Kong routes) when --force is specified or when gitea is detected.
- Installs the GitLab Operator via Helm into the gitlab namespace. - Installs the GitLab Operator via Helm into the gitlab namespace.
- Provisions a GitLab CR that uses the knoe-db CloudNativePG cluster as its - Provisions a GitLab CR that uses the knoe-db CloudNativePG cluster as its
external PostgreSQL data store. 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). - Pins all GitLab workloads to the selected node (default: gandalf.prole.org).
EOF 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_HOST="${GITLAB_REDIS_HOST:-redis-master.${REDIS_NAMESPACE}.svc.cluster.local}"
REDIS_PORT="${GITLAB_REDIS_PORT:-6379}" REDIS_PORT="${GITLAB_REDIS_PORT:-6379}"
# GitLab domain (reuses the git.prole.org shared hostname) # GitLab public domain (configurable; default follows deployment mode)
GITLAB_DOMAIN="${GITLAB_DOMAIN:-git.prole.org}" _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. # Google Workspace OIDC — FRONTDOOR_HOST gates OmniAuth configuration.
# On k3s: api.prole.org is the prole-auth SSO gateway (knoe-auth service). # 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 fi
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Kong ingress for git.prole.org -> gitlab-webservice # Public ingress for configured GitLab domain -> gitlab-webservice
# The GitLab Operator creates the Ingress; this block patches it to Kong class # The GitLab Operator creates the Ingress; this block ensures a stable public
# or creates a fallback Kong-native ingress if the CR-managed one is absent. # class/host mapping if the CR-managed ingress is absent or not usable.
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
WEBSERVICE_SVC="${GITLAB_RELEASE}-webservice-default" 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 kubectl apply -f - <<EOF
apiVersion: networking.k8s.io/v1 apiVersion: networking.k8s.io/v1
@ -967,10 +978,10 @@ metadata:
name: gitlab-kong-ingress name: gitlab-kong-ingress
namespace: ${NAMESPACE} namespace: ${NAMESPACE}
annotations: annotations:
kubernetes.io/ingress.class: kong kubernetes.io/ingress.class: ${GITLAB_INGRESS_CLASS}
konghq.com/strip-path: "false" konghq.com/strip-path: "false"
spec: spec:
ingressClassName: kong ingressClassName: ${GITLAB_INGRESS_CLASS}
rules: rules:
- host: ${GITLAB_DOMAIN} - host: ${GITLAB_DOMAIN}
http: http:

View File

@ -526,6 +526,50 @@ if [[ -n "$_prole_cfg_file" ]]; then
export supabase_hostname="$_cfg_sbh" export supabase_hostname="$_cfg_sbh"
export SUPABASE_HOSTNAME="$_cfg_sbh" export SUPABASE_HOSTNAME="$_cfg_sbh"
fi 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[@]}") _cfg_ctx=$(_prole_cfg_extract_key_in_files "KUBE_CONTEXT_NAME" "${_prole_cfg_files[@]}")
if [[ -z "$_cfg_ctx" ]]; then if [[ -z "$_cfg_ctx" ]]; then
_cfg_ctx=$(_prole_cfg_extract_key_in_files "KUBECTL_CONTEXT" "${_prole_cfg_files[@]}") _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" export KUBECONTEXT="$_cfg_ctx"
fi fi
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 fi
if [[ -z "${PROLE_HOME:-}" && -d "$_prole_cfg_home_guess" ]]; then if [[ -z "${PROLE_HOME:-}" && -d "$_prole_cfg_home_guess" ]]; then

View File

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

View File

@ -2,7 +2,7 @@
""" """
Render a personalized Supabase Helm values file and pre-template manifests Render a personalized Supabase Helm values file and pre-template manifests
using the local prole.cfg. Defaults are tailored for knoe's CNPG cluster 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 from __future__ import annotations
@ -106,6 +106,31 @@ def _parse_hosts(raw_hosts: str, fallback: str = "db.0.knoe.dev") -> list[str]:
return [fallback] 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: def _discover_k8s_service_namespace(service_name: str) -> str:
try: try:
raw = subprocess.check_output(["kubectl", "get", "svc", "-A", "-o", "json"], text=True) 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) anon_key = _jwt(anon_payload, jwt_secret)
service_key = _jwt(service_payload, jwt_secret) service_key = _jwt(service_payload, jwt_secret)
supabase_hostname_raw = _first( legacy_supabase_hostname_raw = _first(
os.environ.get("SUPABASE_STUDIO_URL", ""),
_cfg_get(cfg, "Inputs", "init_cluster.supabase_studio_url"),
_cfg_get(cfg, "User", "supabase_hostname"), _cfg_get(cfg, "User", "supabase_hostname"),
_cfg_get(cfg, "Global", "supabase_hostname"), _cfg_get(cfg, "Global", "supabase_hostname"),
_cfg_get(cfg, "User", "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", default="db.0.knoe.dev",
).strip() ).strip()
if _is_placeholder(supabase_hostname_raw): if _is_placeholder(legacy_supabase_hostname_raw):
supabase_hostname_raw = "" legacy_supabase_hostname_raw = ""
if not supabase_hostname_raw: if not legacy_supabase_hostname_raw:
supabase_hostname_raw = "db.0.knoe.dev" 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( studio_enabled = _as_bool(
_first( _first(
@ -271,30 +322,41 @@ def _build_overlay(cfg: configparser.ConfigParser, args: argparse.Namespace) ->
default=True, default=True,
) )
raw_host_entries: list[str] = [] studio_host_entries, studio_public_url = _normalize_public_hosts(
for entry in supabase_hostname_raw.split(","): studio_hostname_raw,
token = (entry or "").strip() fallback_host="db.0.knoe.dev",
if token and not _is_placeholder(token): )
raw_host_entries.append(token) api_host_entries, api_public_url = _normalize_public_hosts(
if not raw_host_entries: api_hostname_raw,
raw_host_entries = ["db.0.knoe.dev"] fallback_host=studio_host_entries[0],
)
host_entries = _parse_hosts(",".join(raw_host_entries), fallback="db.0.knoe.dev") mode = _first(
ingress_host = host_entries[0] os.environ.get("PROLE_MODE", ""),
_cfg_get(cfg, "Global", "DEPLOYMENT_MODE"),
# Canonical public URL for Supabase (used for auth callbacks + Studio links). _cfg_get(cfg, "globals", "prole.mode"),
public_url = "" default="",
primary_raw_host = (raw_host_entries[0] or "").strip() ).strip().lower()
if "://" in primary_raw_host: default_ingress_class = "gce" if mode == "k8s" else "traefik"
parsed = urllib.parse.urlparse(primary_raw_host) api_ingress_class = _first(
ingress_host = (parsed.hostname or "").strip() or ingress_host os.environ.get("SUPABASE_API_INGRESS_CLASS", ""),
scheme = (parsed.scheme or "https").strip() os.environ.get("SUPABASE_INGRESS_CLASS", ""),
if parsed.netloc: _cfg_get(cfg, "User", "SUPABASE_API_INGRESS_CLASS"),
public_url = f"{scheme}://{parsed.netloc}" _cfg_get(cfg, "Global", "SUPABASE_API_INGRESS_CLASS"),
else: _cfg_get(cfg, "User", "SUPABASE_INGRESS_CLASS"),
public_url = f"{scheme}://{ingress_host}" _cfg_get(cfg, "Global", "SUPABASE_INGRESS_CLASS"),
else: default=default_ingress_class,
public_url = f"https://{ingress_host}" ).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). # 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. # 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"), _cfg_get(cfg, "Supabase", "ADDITIONAL_REDIRECT_URLS"),
default="", default="",
).strip() ).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(","): for _entry in _extra_redirect_raw.split(","):
_entry = _entry.strip() _entry = _entry.strip()
if not _entry or _is_placeholder(_entry): if not _entry or _is_placeholder(_entry):
@ -313,17 +381,11 @@ def _build_overlay(cfg: configparser.ConfigParser, args: argparse.Namespace) ->
_entry = f"https://{_entry}" _entry = f"https://{_entry}"
if not _entry.endswith("/**"): if not _entry.endswith("/**"):
_entry = _entry.rstrip("/") + "/**" _entry = _entry.rstrip("/") + "/**"
_extra_redirect_urls.append(_entry) if _entry not in _allow_list_entries:
# Always include the primary public URL itself; only set if extra origins exist _allow_list_entries.append(_entry)
_gotrue_uri_allow_list = ",".join([public_url + "/**"] + _extra_redirect_urls) if _extra_redirect_urls else "" _gotrue_uri_allow_list = ",".join(_allow_list_entries)
# Placement: pin Supabase pods to the configured primary node (default: pi.knoe.dev) # 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( supabase_primary_node = _first(
os.environ.get("SUPABASE_PRIMARY_NODE", ""), os.environ.get("SUPABASE_PRIMARY_NODE", ""),
os.environ.get("SUPABASE_NODE_SELECTOR", ""), os.environ.get("SUPABASE_NODE_SELECTOR", ""),
@ -427,13 +489,13 @@ def _build_overlay(cfg: configparser.ConfigParser, args: argparse.Namespace) ->
"DB_HOST": db_host, "DB_HOST": db_host,
"DB_PORT": str(db_port), "DB_PORT": str(db_port),
"DB_SSL": "disable", "DB_SSL": "disable",
"API_EXTERNAL_URL": public_url, "API_EXTERNAL_URL": api_public_url,
"GOTRUE_SITE_URL": public_url, "GOTRUE_SITE_URL": studio_public_url,
**( {"GOTRUE_URI_ALLOW_LIST": _gotrue_uri_allow_list} if _gotrue_uri_allow_list else {} ), **( {"GOTRUE_URI_ALLOW_LIST": _gotrue_uri_allow_list} if _gotrue_uri_allow_list else {} ),
}, },
"analytics": {"DB_HOST": db_host, "DB_PORT": str(db_port)}, "analytics": {"DB_HOST": db_host, "DB_PORT": str(db_port)},
"meta": {"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": { "secret": {
"db": {"password": db_password, "database": db_name}, "db": {"password": db_password, "database": db_name},
@ -442,13 +504,13 @@ def _build_overlay(cfg: configparser.ConfigParser, args: argparse.Namespace) ->
}, },
"ingress": { "ingress": {
"enabled": True, "enabled": True,
"className": "traefik", "className": api_ingress_class,
"hosts": [{"host": host, "paths": [{"path": "/", "pathType": "Prefix"}]} for host in host_entries], "hosts": [{"host": host, "paths": [{"path": "/", "pathType": "Prefix"}]} for host in api_host_entries],
}, },
"studioIngress": { "studioIngress": {
"enabled": True, "enabled": True,
"className": "traefik", "className": studio_ingress_class,
"hosts": [{"host": host, "paths": [{"path": "/", "pathType": "Prefix"}]} for host in host_entries], "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.backups.walBucket == "knoe-0-wal"
assert cfg.routing.frontdoorHost == "knoey.com" assert cfg.routing.frontdoorHost == "knoey.com"
assert cfg.routing.platformDomain == "knoe.dev" 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" 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.backups.retentionDays = 0
cfg.migration.sourcePort = 0 cfg.migration.sourcePort = 0
cfg.routing.frontdoorHost = "not_a_domain" cfg.routing.frontdoorHost = "not_a_domain"
cfg.routing.apiHost = "not_a_domain"
errors, _warnings = validate_prod_config(cfg) 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.backups.retentionDays must be a positive number" in errors
assert "spec.migration.sourcePort 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.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(): 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 "pgdataStorageClass: standard-rwo" in yaml_preview
assert "walStorageClass: premium-rwo" in yaml_preview assert "walStorageClass: premium-rwo" in yaml_preview
assert "frontdoorHost: knoey.com" 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["project_id"] == "my-gcp-project"
assert tf_vars["namespace"] == "ecosystem-0" assert tf_vars["namespace"] == "ecosystem-0"
assert tf_vars["cnpg_cluster_name"] == "knoe-db" 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_wal_storage_class"] == "premium-rwo"
assert tf_vars["db_pgdata_size_gi"] == 120 assert tf_vars["db_pgdata_size_gi"] == 120
assert tf_vars["db_wal_size_gi"] == 32 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(): 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] [Global]
NAMESPACE = test-ns NAMESPACE = test-ns
STORAGE_BACKEND = local
""", """,
) )
@ -77,6 +78,7 @@ def test_render_supabase_allows_scheme_in_supabase_hostname(tmp_path: Path, monk
[Global] [Global]
NAMESPACE = test-ns 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["ingress"]["hosts"][0]["host"] == "db.prole.org"
assert overlay["environment"]["auth"]["API_EXTERNAL_URL"] == "http://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" 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/**"
)