chore: checkpoint k3s deployment and knoe updates

- update Kong/Supabase ingress and service manifests for k3s

- expand init scripts and runtime config/topology handling

- refresh KNØE UI screens and Supabase deployment/render logic

- include related env helper test adjustments

Co-authored-by: Junie <junie@jetbrains.com>
This commit is contained in:
chrisfu 2026-03-31 20:40:41 -07:00
parent c0a7d0c5dc
commit 93f7259722
27 changed files with 1018 additions and 122 deletions

View File

@ -2,6 +2,12 @@
# Format: key: local=... remote=... ns=... svc=... address=... # Format: key: local=... remote=... ns=... svc=... address=...
argocd: local=8081 remote=80 ns=argocd svc=argocd-server address=0.0.0.0 argocd: local=8081 remote=80 ns=argocd svc=argocd-server address=0.0.0.0
supabase-studio: local=18080 remote=3000 ns=supabase svc=studio address=0.0.0.0
supabase-auth: local=9999 remote=9999 ns=supabase svc=auth address=127.0.0.1
supabase-rest: local=3001 remote=3000 ns=supabase svc=rest address=0.0.0.0
supabase-realtime: local=4000 remote=4000 ns=supabase svc=realtime address=0.0.0.0
gitea-http: local=13000 remote=3000 ns=gitea svc=gitea-http address=0.0.0.0
gitea-ssh: local=22 remote=22 ns=gitea svc=gitea-ssh address=0.0.0.0
garage: local=3900 remote=3900 ns=knoe-system svc=garage address=0.0.0.0 garage: local=3900 remote=3900 ns=knoe-system svc=garage address=0.0.0.0
openbao: local=8200 remote=8200 ns=knoe-system svc=openbao address=0.0.0.0 openbao: local=8200 remote=8200 ns=knoe-system svc=openbao address=0.0.0.0
opentofu: local=8080 remote=8080 ns=knoe-system svc=opentofu address=0.0.0.0 opentofu: local=8080 remote=8080 ns=knoe-system svc=opentofu address=0.0.0.0
@ -9,3 +15,4 @@ dashboard: local=8443 remote=443 ns=kubernetes-dashboard svc=kubernetes-dashboar
postgres: local=5432 remote=5432 ns=knoe-db svc=knoe-db-rw address=0.0.0.0 postgres: local=5432 remote=5432 ns=knoe-db svc=knoe-db-rw address=0.0.0.0
prometheus: local=9090 remote=9090 ns=monitoring svc=kps-kube-prometheus-stack-prometheus address=127.0.0.1 prometheus: local=9090 remote=9090 ns=monitoring svc=kps-kube-prometheus-stack-prometheus address=127.0.0.1
grafana: local=3000 remote=80 ns=monitoring svc=kps-grafana address=0.0.0.0 grafana: local=3000 remote=80 ns=monitoring svc=kps-grafana address=0.0.0.0
supabase-kong: local=8000 remote=8000 ns=supabase svc=kong address=0.0.0.0

View File

@ -1,5 +1,5 @@
; Prole Master Configuration File ; Prole Master Configuration File
; Generated by install.py on 2026-03-31 00:16:16 ; Generated by install.py on 2026-03-31 18:38:57
; This file is used as input for Ansible deployment and k8s cluster creation. ; This file is used as input for Ansible deployment and k8s cluster creation.
[User] [User]
@ -8,6 +8,7 @@
[Inputs] [Inputs]
; Screen-scoped inputs used for unattended replays (-S) ; Screen-scoped inputs used for unattended replays (-S)
argocd.node_selector = gandalf.prole.org
build.deploy_env = Dev build.deploy_env = Dev
build.run_build = false build.run_build = false
database_options.distribution = percona database_options.distribution = percona
@ -47,33 +48,41 @@ env_setup.PROLE_DATA = /Users/chrisfu/dev/prole/data
env_setup.PROLE_HOME = /Users/chrisfu/dev/prole env_setup.PROLE_HOME = /Users/chrisfu/dev/prole
env_setup.PROLE_LOGS = /Users/chrisfu/dev/prole/logs env_setup.PROLE_LOGS = /Users/chrisfu/dev/prole/logs
env_setup.PROLE_SERVICE = /Users/chrisfu/dev/prole/etc env_setup.PROLE_SERVICE = /Users/chrisfu/dev/prole/etc
gitops.node_selector = gandalf.prole.org
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 = service init_cluster.cluster_env = service
init_cluster.deployment_target = prole-service-cluster init_cluster.deployment_target = prole-service-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:AL731QFqjjFGqfFT:Xt95lYCIk-6iCHerFSJQpKwWGJMdr04v-4DawlGbM3OBLJ7qbHOSZ_oJm1WtHkvqpoM7BSJNwlEqHMV7R3gl7IFqVUtqJ_AoaRcLSPKE2Z6JGWkIatR6Vzw3J1cm1iZiM2M9wikI5Wqp3t4cdWTOTf7PUaQHkYbBG1PV3w==} init_cluster.k3s_token = ${PROLE_SECRET:v1:m0esKgX4z-UxYI3V:Rtau88iOMq7zuzT4A9y5PnaWy80PrSqFmoo9vqqXAsOORqFIM1CdcDyFdkHxmIPKTsSEnj91LXmRr3mie83lq4Y-qRj1PMmDD5f3yAtvFA33i709FunDECChkJLVoZJrDHGnRuSaA5ZHwIvqNXRY36S4LxCKluHk6scy8A==}
init_cluster.kerberos_enabled = true init_cluster.kerberos_enabled = true
init_cluster.mode = k3s init_cluster.mode = k3s
init_cluster.start_cluster = true init_cluster.start_cluster = true
init_cluster.supabase_enabled = false init_cluster.supabase_analytics_enabled = true
init_cluster.supabase_auth_enabled = true
init_cluster.supabase_enabled = true
init_cluster.supabase_meta_enabled = true
init_cluster.supabase_realtime_enabled = true
init_cluster.supabase_studio_enabled = false
init_cluster.supabase_studio_url = db.prole.org
init_cnpg_deploy.force_rollout = false init_cnpg_deploy.force_rollout = false
init_cnpg_deploy.run_deploy = true init_cnpg_deploy.run_deploy = true
init_db_build.run_build = true init_db_build.run_build = true
init_password.cluster_name = ${CLUSTER_NAME} init_password.cluster_name = ${CLUSTER_NAME}
init_password.db_host_port = 5432 init_password.db_host_port = 5432
init_password.db_namespace = ${DATABASE_NAMESPACE} init_password.db_namespace = ${DATABASE_NAMESPACE}
init_password.db_password = ${PROLE_SECRET:v1:OcWBTOiijqf-bfTT:6r-ioWdDRdkPNxmbEAQP2TYEPaycWasP} init_password.db_password = ${PROLE_SECRET:v1:iC68SlzGpoPl9nma:iQEsd-2BzbTXdxTnSldMjHmrFrij_hWJ}
init_password.db_password_confirm = ${PROLE_SECRET:v1:OcWBTOiijqf-bfTT:6r-ioWdDRdkPNxmbEAQP2TYEPaycWasP} init_password.db_password_confirm = ${PROLE_SECRET:v1:iC68SlzGpoPl9nma:iQEsd-2BzbTXdxTnSldMjHmrFrij_hWJ}
init_password.db_username = root init_password.db_username = root
init_password.generate_ssh_key = true init_password.generate_ssh_key = true
init_scripts.run_scripts = true init_scripts.run_scripts = true
kerberos_config.enabled = true kerberos_config.enabled = true
kerberos_config.init_authority = false
kerberos_config.kdc = 10.0.0.3 kerberos_config.kdc = 10.0.0.3
kerberos_config.password = ${PROLE_SECRET:v1:h9mzEWvGpnpU2FRz:5W8czWfjnRxzA8tJPk3eFRtrVLD5DxREkfqmQsnKLsbuAaec} kerberos_config.password = ${PROLE_SECRET:v1:U-EPMl7qv4heEB1k:BEcXGbI_LT4lCXwBuvFEGgPbLFa9MpVzECObdH0pbLtClDHf}
kerberos_config.realm = PROLE.ORG kerberos_config.realm = PROLE.ORG
kerberos_config.test_connection = false kerberos_config.test_connection = true
kerberos_config.user = administrator kerberos_config.user = administrator
network_scan.run = true network_scan.run = true
ollama_config.model = ollama_config.model =
@ -85,6 +94,7 @@ supabase_config.pv_node = gandalf.prole.org
[Global] [Global]
; Variables used by name in more than one place or assumed global scope ; Variables used by name in more than one place or assumed global scope
ARGOCD_NAMESPACE = argocd ARGOCD_NAMESPACE = argocd
ARGOCD_NODE_SELECTOR = gandalf.prole.org
CLUSTER_ENV = service CLUSTER_ENV = service
CLUSTER_NAME = knoe-db CLUSTER_NAME = knoe-db
CNPG_ELIGIBLE_NODES = gandalf.prole.org,merlin.prole.org,myrddin.prole.org,pi.prole.org CNPG_ELIGIBLE_NODES = gandalf.prole.org,merlin.prole.org,myrddin.prole.org,pi.prole.org
@ -94,15 +104,22 @@ CNPG_PLACEMENT_PLAN_ID = cnpg-placement-7036c5f65b6ef0db
CNPG_STAGE1_NODE = gandalf.prole.org CNPG_STAGE1_NODE = gandalf.prole.org
DATABASE_NAMESPACE = knoe-db DATABASE_NAMESPACE = knoe-db
DB_HOST_PORT = 5432 DB_HOST_PORT = 5432
DB_PASSWORD = ${PROLE_SECRET:v1:M7Rt2Xp273X8ixb5:Rjys8A6SqmBv19DvPAvgihwJ0eJ8Xvda} DB_PASSWORD = ${PROLE_SECRET:v1:t7NgRHfXTXH-mHcx:CRbKP9b6ccuxrTMMeJ742Q48_vegoUY4}
DEPLOYMENT_MODE = k3s DEPLOYMENT_MODE = k3s
DEPLOYMENT_TARGET = prole-service-cluster DEPLOYMENT_TARGET = prole-service-cluster
DOCKER_IMPORT_DIR =
DOCKER_PRELOAD = false DOCKER_PRELOAD = false
GITEA_NODE_SELECTOR = gandalf.prole.org
K3S_SERVER = https://myrddin.prole.org:6443
K3S_TOKEN = ${PROLE_SECRET:v1:gnQjvO_dhM7qkU_g:kFb7E6IQ40RM64JfsrJWBPzkJsMaLXNpNPxfho6t8w5KAeWCAyrw2bMsQtJ8n7cPc_SLWh_zX53Sa1pvEOclmd79e-c_Qff0gZleba1PzzfkjsYRfVlpzQ34gdAHrNHQG2mKnpkcyrNFZ0j6J-w4c3m_m6bTN1cV1KQvMQ==}
KNOE_DB_USER = root KNOE_DB_USER = root
KUBECONTEXT = knoe.dev.prole.org KUBECONTEXT = knoe.dev.prole.org
OPENTOFU_URL = http://myrddin.prole.org:8080
OPTIONAL_WORKLOADS_MIN_READY_SCHEDULABLE_NODES = 2 OPTIONAL_WORKLOADS_MIN_READY_SCHEDULABLE_NODES = 2
PROLE_HOME = /Users/chrisfu/dev/prole
PROLE_K3S_SERVER = https://myrddin.prole.org:6443 PROLE_K3S_SERVER = https://myrddin.prole.org:6443
PROLE_K3S_TOKEN = ${PROLE_SECRET:v1:DkOrjdIC-ji9iusP:7uc4RUXpALlyQcGfZfX6LBP7BEwOqBtbbZpbIgdbfJvMQ_1klRdLtDQrcG54p6ayHvaMQ6W7FSPHPLeKbcjDZ8eJMhNMaH9Q0kIyfmiZdbX3nN6QGCxJWjIn-4GJ4z6_gF7hjHrqAAqxReNoWkAokyoVgqKTiOoWToD0GA==} PROLE_K3S_TOKEN = K107c8c6000488eca4a067d8a73119bbae2f07b4ea1bac7d8d3dc9c500cbb8acb18::server:04572345810eae2f9619a6ed4239702b
REGISTRY_NAMESPACE = knoe-system
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
@ -113,7 +130,7 @@ SYNOLOGY_ROOTS = /synology/d001,/synology/d002,/synology/d003,/synology/d004,/sy
; No configuration values captured yet for this section. ; No configuration values captured yet for this section.
[Dependencies] [Dependencies]
; No configuration values captured yet for this section. STATUS = All installed
[Network] [Network]
AD_DC_HOST = myrddin.prole.org AD_DC_HOST = myrddin.prole.org
@ -128,7 +145,21 @@ KDC_AUTO_DETECTED = 10.0.0.3
KERBEROS_AUTO_ENABLED = True KERBEROS_AUTO_ENABLED = True
[Port Forwards] [Port Forwards]
; No configuration values captured yet for this section. PORT_FORWARD_K3S_MAPPING_1 = id=argocd;namespace=argocd;target=svc/argocd-server;address=0.0.0.0;hostPort=8081;servicePort=80;protocol=TCP;description=ArgoCD
PORT_FORWARD_K3S_MAPPING_10 = id=supabase-studio;namespace=supabase;target=svc/studio;address=0.0.0.0;hostPort=18080;servicePort=3000;protocol=TCP;description=Supabase Studio
PORT_FORWARD_K3S_MAPPING_11 = id=supabase-auth;namespace=supabase;target=svc/auth;address=127.0.0.1;hostPort=9999;servicePort=9999;protocol=TCP;description=Supabase Auth (GoTrue)
PORT_FORWARD_K3S_MAPPING_12 = id=supabase-rest;namespace=supabase;target=svc/rest;address=0.0.0.0;hostPort=3001;servicePort=3000;protocol=TCP;description=Supabase REST (PostgREST)
PORT_FORWARD_K3S_MAPPING_13 = id=supabase-realtime;namespace=supabase;target=svc/realtime;address=0.0.0.0;hostPort=4000;servicePort=4000;protocol=TCP;description=Supabase Realtime
PORT_FORWARD_K3S_MAPPING_14 = id=gitea-http;namespace=gitea;target=svc/gitea-http;address=0.0.0.0;hostPort=13000;servicePort=3000;protocol=TCP;description=Gitea Web
PORT_FORWARD_K3S_MAPPING_15 = id=gitea-ssh;namespace=gitea;target=svc/gitea-ssh;address=0.0.0.0;hostPort=22;servicePort=22;protocol=TCP;description=Gitea SSH
PORT_FORWARD_K3S_MAPPING_2 = id=garage;namespace=knoe-system;target=svc/garage;address=0.0.0.0;hostPort=3900;servicePort=3900;protocol=TCP;description=Garage S3
PORT_FORWARD_K3S_MAPPING_3 = id=openbao;namespace=knoe-system;target=svc/openbao;address=0.0.0.0;hostPort=8200;servicePort=8200;protocol=TCP;description=OpenBao
PORT_FORWARD_K3S_MAPPING_4 = id=opentofu;namespace=knoe-system;target=svc/opentofu;address=0.0.0.0;hostPort=8080;servicePort=8080;protocol=TCP;description=OpenTofu
PORT_FORWARD_K3S_MAPPING_5 = id=dashboard;namespace=kubernetes-dashboard;target=svc/kubernetes-dashboard-kong-proxy;address=127.0.0.1;hostPort=8443;servicePort=443;protocol=TCP;description=Kubernetes Dashboard
PORT_FORWARD_K3S_MAPPING_6 = id=postgres;namespace=${DATABASE_NAMESPACE};target=svc/knoe-db-rw;address=0.0.0.0;hostPort=5432;servicePort=5432;protocol=TCP;description=PostgreSQL (primary)
PORT_FORWARD_K3S_MAPPING_7 = id=prometheus;namespace=monitoring;target=svc/kps-kube-prometheus-stack-prometheus;address=127.0.0.1;hostPort=9090;servicePort=9090;protocol=TCP;description=Prometheus UI
PORT_FORWARD_K3S_MAPPING_8 = id=grafana;namespace=monitoring;target=svc/kps-grafana;address=0.0.0.0;hostPort=3000;servicePort=80;protocol=TCP;description=Grafana UI
PORT_FORWARD_K3S_MAPPING_9 = id=supabase-kong;namespace=supabase;target=svc/kong;address=0.0.0.0;hostPort=8000;servicePort=8000;protocol=TCP;description=Supabase API (Kong)
[System Environment] [System Environment]
PROLE_CONF = /Users/chrisfu/dev/prole/conf PROLE_CONF = /Users/chrisfu/dev/prole/conf
@ -138,10 +169,22 @@ PROLE_LOGS = /Users/chrisfu/dev/prole/logs
PROLE_SERVICE = /Users/chrisfu/dev/prole/etc PROLE_SERVICE = /Users/chrisfu/dev/prole/etc
[Monitoring] [Monitoring]
GRAFANA_ADMIN_PASSWORD = ${PROLE_SECRET:v1:st2uaIMwx55ftSPt:HjzyDuKpzGJyIDVzIKWD6YDzyoqZ4iJ9} GRAFANA_ADMIN_PASSWORD = ${PROLE_SECRET:v1:L2BhAyHRKVei4cGM:-J83-HR_CpnPFpiB6NbOXKdmmagVW2qL}
[Kerberos Authentication] [Kerberos Authentication]
; No configuration values captured yet for this section. AD_PORT_FORWARD = 1
AD_PROXY_HOST_NETWORK = 1
AD_PROXY_IMAGE = alpine/socat
AD_PROXY_SERVICE = prole-kerberos-ad-dc
AD_TCP_PORTS = 88 389 445 464 636
AD_UDP_PORTS = 88 464
ENABLED = True
KDC = 10.0.0.3
PASSWORD = ${PROLE_SECRET:v1:jJ4tvCF8ef8xxQ-o:82chD7iVrlUNaDDA8K5DgYMWO2DN9FUSE7Rrm8IVhwuapK2P}
REALM = PROLE.ORG
SERVER = 10.0.0.3
STATUS = Initialized
USER = administrator
[Ollama] [Ollama]
OLLAMA_HOST = http://fairyland.prole.org:11434 OLLAMA_HOST = http://fairyland.prole.org:11434
@ -149,40 +192,47 @@ OLLAMA_SERVER_HOST = fairyland.prole.org
OLLAMA_SERVER_PORT = 11434 OLLAMA_SERVER_PORT = 11434
[Optional Features] [Optional Features]
AT_REST_ENCRYPTION_ENABLED = True ARGOCD_ENABLED = False
GITOPS_ENABLED = False AT_REST_ENCRYPTION_ENABLED = true
KERBEROS_ENABLED = True GITOPS_ENABLED = true
SUPABASE_ENABLED = False GITOPS_PROVIDER = gitea
KERBEROS_ENABLED = true
SUPABASE_ENABLED = true
[GitOps] [GitOps]
; No configuration values captured yet for this section. STATUS = Deployed
[Database Creation] [Database Creation]
; No configuration values captured yet for this section. DB_USER = root
[Initialize Cluster] [Initialize Cluster]
ENVIRONMENT = service ENVIRONMENT = service
K3S_SERVER_URL = https://myrddin.prole.org:6443 K3S_SERVER_URL = https://myrddin.prole.org:6443
K3S_TOKEN = ${PROLE_SECRET:v1:sQeWAX7UamXXV4oR:O7sUJk54DE9gUtZAjmN4uiusuVl0T3SDTn-S4Ojgg844EOQV7dMhE1wncgn8kLXXz_klqkQszDzbTqQm2HdaQuFyCqnHGCtHyUAyFvXSAdhhQ3PSkggylLtbPQPPrEP6WFuFywdKf1QgLl2BOcipREupBj5XDfiNfEAigw==} K3S_TOKEN = ${PROLE_SECRET:v1:HAOwKPWbbBvJ7JUN:fuseXaNaQzGdzklTyEelMLldsma1MX7Ciq2P-nzrlsDXpJy9DXv4i4siVOa4aPIYvKP66fAh8IGfk_gwX0JwGQorSgGrgRpaEKjBtZ-0sh48AJPFkP7VkpQw4QZr5xzoDgAA96Q3r70TgDFHlo81OV8_riptpqYp5BO5nA==}
[Dev Cluster (k3d)] [Dev Cluster (k3d)]
CLUSTER_ENV = dev CLUSTER_ENV = k3d-knoe-dev-cluster
DISPLAY_NAME = knoe-dev-cluster DISPLAY_NAME = knoe-dev-cluster
KUBECTL_CONTEXT = knoe.dev.prole.org KUBECTL_CONTEXT = service
MODE = k3d MODE = k3d
[Service Cluster (k3s)] [Service Cluster (k3s)]
CLUSTER_ENV = prole-service-cluster CLUSTER_ENV = prole-service-cluster
DISPLAY_NAME = prole-service-cluster DISPLAY_NAME = prole-service-cluster
K3S_SERVER_URL = https://myrddin.prole.org:6443 K3S_SERVER_URL = https://myrddin.prole.org:6443
K3S_TOKEN = ${PROLE_SECRET:v1:sast5Yrr_9mB5BzC:J8Et0XHuUbZbFzAovq-lgqdu_3rR4k3FHOJ7LmzCls-qcZV9D9_NNdmtw5gOTRA8kyZeP5oAFGAXKPGWHgxMJxGO_Lh2oQ1m_68_ExKCK7oeXxlZC3bFCd_fvQ-lP0thBYA9R-GkV_67tJfluOo632xhv6i3Pl1sgBZt5w==} K3S_TOKEN = ${PROLE_SECRET:v1:-5rud_XHzHRKoZIU:Dxv2nKbY883uDrJlmjRIqUKTtF42KPwXmzONy1s0-cIsJ2TLpTkaSWjfYx0tXNnz65lPnwIR16kXVtICYHhOxhfSIaz-lQiNGwZ_hm1tGMjp9BvpANt_l5Ie6swbOHVw8QfpGLtiXuIv7ur2ocSX4xAYSdCKtYWWV58U0Q==}
MODE = k3s MODE = k3s
PIPELINE_URL = http://myrddin.prole.org:8080
[GCP]
; No configuration values captured yet for this section.
[Prod Cluster (k8s)] [Prod Cluster (k8s)]
ARTIFACTS_DIR = /Users/chrisfu/dev/prole/data/staging ARTIFACTS_DIR =
CLUSTER_ENV = prole-prod-cluster CLUSTER_ENV = prole-prod-cluster
DISPLAY_NAME = prole-prod-cluster DISPLAY_NAME = prole-prod-cluster
MODE = k8s MODE = k8s
PIPELINE_URL = http://myrddin.prole.org:8080
[Docker Build] [Docker Build]
; No configuration values captured yet for this section. ; No configuration values captured yet for this section.
@ -195,4 +245,4 @@ MODE = k3s
TARGET = prole-service-cluster TARGET = prole-service-cluster
[Install] [Install]
; No configuration values captured yet for this section. STATUS = Finished

View File

@ -66,3 +66,13 @@ spec:
name: kong name: kong
port: port:
number: 8000 number: 8000
- host: supabase.prole.org
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: kong
port:
number: 8000

View File

@ -10,10 +10,63 @@ data:
_transform: true _transform: true
services: services:
- name: prole-service
url: http://prole-svc.knoe-db.svc.cluster.local:8080
routes:
- name: prole-k3s-kubeconfig
hosts:
- svc.prole.org
paths:
- /k3s/kube_config.sh
strip_path: false
- name: db-manager - name: db-manager
url: http://knoe-db-manager:80 url: http://knoe-db-manager:80
routes: routes:
- name: backup-route - name: backup-route
hosts:
- svc.prole.org
paths: paths:
- /backup - /backup
strip_path: false strip_path: false
- name: grafana
url: http://kps-grafana.monitoring.svc.cluster.local:80
routes:
- name: grafana-root
hosts:
- svc.prole.org
paths:
- /
strip_path: false
- name: knoe-auth
url: http://knoe-auth.knoe-system.svc.cluster.local:8080
routes:
- name: knoe-auth-root
hosts:
- api.prole.org
paths:
- /
strip_path: false
- name: gitea-http
url: http://gitea-http.gitea.svc.cluster.local:3000
routes:
- name: gitea-root
hosts:
- git.prole.org
paths:
- /
strip_path: false
- name: gitea-ssh
host: gitea-ssh.gitea.svc.cluster.local
port: 22
protocol: tcp
routes:
- name: gitea-ssh-tcp
protocols:
- tcp
destinations:
- port: 3022

View File

@ -25,6 +25,8 @@ spec:
name: proxy name: proxy
- containerPort: 8001 - containerPort: 8001
name: admin name: admin
- containerPort: 3022
name: ssh-proxy
env: env:
- name: KONG_DATABASE - name: KONG_DATABASE
value: "off" value: "off"
@ -32,6 +34,8 @@ spec:
value: "0.0.0.0:8000" value: "0.0.0.0:8000"
- name: KONG_ADMIN_LISTEN - name: KONG_ADMIN_LISTEN
value: "0.0.0.0:8001" value: "0.0.0.0:8001"
- name: KONG_STREAM_LISTEN
value: "0.0.0.0:3022 reuseport backlog=16384"
- name: KONG_DECLARATIVE_CONFIG - name: KONG_DECLARATIVE_CONFIG
value: "/etc/kong/declarative/kong.yml" value: "/etc/kong/declarative/kong.yml"
volumeMounts: volumeMounts:

View File

@ -16,4 +16,7 @@ spec:
- name: admin - name: admin
port: 8001 port: 8001
targetPort: admin targetPort: admin
- name: ssh-proxy
port: 3022
targetPort: ssh-proxy
type: ClusterIP type: ClusterIP

View File

@ -17,3 +17,13 @@ spec:
name: kong name: kong
port: port:
number: 8000 number: 8000
- host: supabase.prole.org
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: kong
port:
number: 8000

290
etc/init_gitea.sh Normal file → Executable file
View File

@ -81,6 +81,118 @@ RELEASE_NAME="gitea"
IMAGE_REPO_DEFAULT="gitea/gitea" IMAGE_REPO_DEFAULT="gitea/gitea"
IMAGE_TAG="${GITEA_IMAGE_TAG:-1.22.3}" IMAGE_TAG="${GITEA_IMAGE_TAG:-1.22.3}"
IMAGE_REPO="$IMAGE_REPO_DEFAULT" IMAGE_REPO="$IMAGE_REPO_DEFAULT"
NODE_SELECTOR="${GITEA_NODE_SELECTOR:-${NODE_SELECTOR:-}}"
GITEA_PV_NODE="${GITEA_PV_NODE:-${GITEA_NODE_SELECTOR:-gandalf.prole.org}}"
GITEA_PV_BASE_DIR="${GITEA_PV_BASE_DIR:-/synology/d005}"
GITEA_STORAGE_CLASS="${GITEA_STORAGE_CLASS:-gitea-local-d005}"
GITEA_DOMAIN="${GITEA_DOMAIN:-git.prole.org}"
GITEA_SSH_DOMAIN="${GITEA_SSH_DOMAIN:-$GITEA_DOMAIN}"
KNOE_DB_NAMESPACE="${KNOE_DB_NAMESPACE:-${DATABASE_NAMESPACE:-knoe-db}}"
KNOE_DB_CLUSTER="${KNOE_DB_CLUSTER:-${CLUSTER_NAME:-knoe-db}}"
KNOE_DB_SERVICE="${KNOE_DB_SERVICE:-${KNOE_DB_CLUSTER}-rw}"
KNOE_DB_PORT="${KNOE_DB_PORT:-${DB_HOST_PORT:-5432}}"
KNOE_DB_ADMIN_USER="${KNOE_DB_ADMIN_USER:-${KNOE_DB_USER:-postgres}}"
GITEA_DB_NAME="${GITEA_DB_NAME:-gitea}"
GITEA_DB_USER="${GITEA_DB_USER:-gitea}"
GITEA_DB_PASSWORD="${GITEA_DB_PASSWORD:-${DB_PASSWORD:-}}"
is_secret_placeholder() {
case "${1:-}" in
'${PROLE_SECRET:'*|'${OPENBAO:'*) return 0 ;;
esac
return 1
}
resolve_gitea_db_password() {
if [[ -n "${GITEA_DB_PASSWORD:-}" ]] && ! is_secret_placeholder "${GITEA_DB_PASSWORD}"; then
return 0
fi
if kubectl -n "$KNOE_DB_NAMESPACE" get secret knoe-db-superuser >/dev/null 2>&1; then
local resolved
resolved=$(kubectl -n "$KNOE_DB_NAMESPACE" get secret knoe-db-superuser \
-o jsonpath='{.data.password}' 2>/dev/null | base64 -d 2>/dev/null || true)
if [[ -n "$resolved" ]]; then
GITEA_DB_PASSWORD="$resolved"
fi
fi
if [[ -z "${GITEA_DB_PASSWORD:-}" ]] || is_secret_placeholder "${GITEA_DB_PASSWORD}"; then
warn "Could not resolve a concrete Gitea DB password; database bootstrap may be skipped."
fi
}
resolve_knoe_db_primary_pod() {
local primary
primary=$(kubectl -n "$KNOE_DB_NAMESPACE" get pods \
-l "cnpg.io/cluster=${KNOE_DB_CLUSTER},cnpg.io/instanceRole=primary" \
-o jsonpath='{.items[0].metadata.name}' 2>/dev/null || true)
if [[ -z "$primary" ]]; then
primary=$(kubectl -n "$KNOE_DB_NAMESPACE" get pods \
-l "cnpg.io/cluster=${KNOE_DB_CLUSTER}" \
-o jsonpath='{.items[0].metadata.name}' 2>/dev/null || true)
fi
printf '%s' "$primary"
}
sql_escape_literal() {
printf '%s' "${1:-}" | sed "s/'/''/g"
}
setup_knoe_db_for_gitea() {
local primary
primary="$(resolve_knoe_db_primary_pod)"
if [[ -z "$primary" ]]; then
warn "No knoe-db pod found in namespace '${KNOE_DB_NAMESPACE}'; skipping Gitea DB setup."
return 0
fi
resolve_gitea_db_password
if [[ -z "${GITEA_DB_PASSWORD:-}" ]] || is_secret_placeholder "${GITEA_DB_PASSWORD}"; then
warn "Skipping Gitea DB setup due to unresolved GITEA_DB_PASSWORD."
return 0
fi
local admin_user=""
local candidate
for candidate in "$KNOE_DB_ADMIN_USER" postgres root; do
[[ -z "$candidate" ]] && continue
if kubectl -n "$KNOE_DB_NAMESPACE" exec "$primary" -c postgres -- \
psql -U "$candidate" -d postgres -tAc "SELECT 1" >/dev/null 2>&1; then
admin_user="$candidate"
break
fi
done
if [[ -z "$admin_user" ]]; then
warn "Unable to connect to knoe-db as admin user; skipping Gitea DB setup."
return 0
fi
local escaped_password
escaped_password="$(sql_escape_literal "$GITEA_DB_PASSWORD")"
kubectl -n "$KNOE_DB_NAMESPACE" exec "$primary" -c postgres -- \
psql -U "$admin_user" -d postgres -c "
DO \$\$ BEGIN
IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname='${GITEA_DB_USER}') THEN
CREATE ROLE ${GITEA_DB_USER} LOGIN PASSWORD '${escaped_password}';
END IF;
END \$\$;
" >/dev/null 2>&1 || warn "Could not create role '${GITEA_DB_USER}'."
local db_exists
db_exists=$(kubectl -n "$KNOE_DB_NAMESPACE" exec "$primary" -c postgres -- \
psql -U "$admin_user" -d postgres -tAc "SELECT 1 FROM pg_database WHERE datname='${GITEA_DB_NAME}';" 2>/dev/null || true)
if [[ "$db_exists" != "1" ]]; then
kubectl -n "$KNOE_DB_NAMESPACE" exec "$primary" -c postgres -- \
psql -U "$admin_user" -d postgres -c "CREATE DATABASE ${GITEA_DB_NAME} OWNER ${GITEA_DB_USER};" \
>/dev/null 2>&1 || warn "Could not create database '${GITEA_DB_NAME}'."
fi
log "Gitea database '${GITEA_DB_NAME}' prepared in namespace '${KNOE_DB_NAMESPACE}'."
}
# Optional reset # Optional reset
if [[ "$FORCE" -eq 1 ]]; then if [[ "$FORCE" -eq 1 ]]; then
@ -119,20 +231,177 @@ deploy_with_helm() {
# Minimal config: NodePort or ClusterIP + Ingress depending on environment. # Minimal config: NodePort or ClusterIP + Ingress depending on environment.
# Avoid heavy persistence defaults; users can override via values later. # Avoid heavy persistence defaults; users can override via values later.
local -a helm_args=(
--set "image.repository=$IMAGE_REPO"
--set "image.tag=$IMAGE_TAG"
--set "image.rootless=false"
--set "image.fullOverride=${IMAGE_REPO}:${IMAGE_TAG}"
--set "service.http.type=ClusterIP"
--set "service.ssh.type=ClusterIP"
--set "service.ssh.port=22"
--set "gitea.admin.username=${GITEA_ADMIN_USER:-gitea_admin}"
--set "gitea.admin.password=${GITEA_ADMIN_PASSWORD:-gitea_admin}"
--set "gitea.admin.email=${GITEA_ADMIN_EMAIL:-gitea_admin@example.local}"
--set "gitea.config.server.DOMAIN=${GITEA_DOMAIN}"
--set "gitea.config.server.SSH_DOMAIN=${GITEA_SSH_DOMAIN}"
--set "gitea.config.server.ROOT_URL=http://${GITEA_DOMAIN}/"
--set "gitea.config.server.HTTP_PORT=3000"
--set "gitea.config.server.SSH_PORT=22"
--set "gitea.config.database.DB_TYPE=postgres"
--set "gitea.config.database.HOST=${KNOE_DB_SERVICE}.${KNOE_DB_NAMESPACE}.svc.cluster.local:${KNOE_DB_PORT}"
--set "gitea.config.database.NAME=${GITEA_DB_NAME}"
--set "gitea.config.database.USER=${GITEA_DB_USER}"
--set-string "gitea.config.database.PASSWD=${GITEA_DB_PASSWORD}"
--set "gitea.config.database.SSL_MODE=disable"
--set "gitea.config.session.PROVIDER=db"
--set "postgresql.enabled=false"
--set "postgresql-ha.enabled=false"
--set "valkey-cluster.enabled=false"
--set "persistence.enabled=true"
)
if [[ "$MODE" == "k3s" ]]; then
helm_args+=(
--set "gitea.config.cache.ADAPTER=memory"
--set "global.storageClass=$GITEA_STORAGE_CLASS"
--set "persistence.storageClass=$GITEA_STORAGE_CLASS"
--set "nodeSelector.kubernetes\\.io/hostname=$GITEA_PV_NODE"
--set "deployment.strategy=Recreate"
)
fi
helm upgrade --install "$RELEASE_NAME" "$CHART_NAME" \ helm upgrade --install "$RELEASE_NAME" "$CHART_NAME" \
-n "$NAMESPACE" \ -n "$NAMESPACE" \
--set image.repository="$IMAGE_REPO" \ "${helm_args[@]}" \
--set image.tag="$IMAGE_TAG" \
--set service.http.type=ClusterIP \
--set service.ssh.type=ClusterIP \
--set gitea.admin.username="${GITEA_ADMIN_USER:-gitea_admin}" \
--set gitea.admin.password="${GITEA_ADMIN_PASSWORD:-gitea_admin}" \
--set gitea.admin.email="${GITEA_ADMIN_EMAIL:-gitea_admin@example.local}" \
--wait --timeout 10m --wait --timeout 10m
} }
ensure_k3s_storage_layout() {
[[ "$MODE" == "k3s" ]] || return 0
log "Ensuring Gitea k3s storage on node '${GITEA_PV_NODE}' at '${GITEA_PV_BASE_DIR}' (storageClass=${GITEA_STORAGE_CLASS})"
cat <<EOF | kubectl apply -f -
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: ${GITEA_STORAGE_CLASS}
provisioner: kubernetes.io/no-provisioner
reclaimPolicy: Retain
volumeBindingMode: WaitForFirstConsumer
EOF
cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: PersistentVolume
metadata:
name: gitea-d005-shared-storage
spec:
capacity:
storage: 10Gi
accessModes:
- ReadWriteOnce
storageClassName: ${GITEA_STORAGE_CLASS}
persistentVolumeReclaimPolicy: Retain
hostPath:
path: ${GITEA_PV_BASE_DIR}/gitea/shared
type: DirectoryOrCreate
claimRef:
namespace: ${NAMESPACE}
name: gitea-shared-storage
nodeAffinity:
required:
nodeSelectorTerms:
- matchExpressions:
- key: kubernetes.io/hostname
operator: In
values:
- ${GITEA_PV_NODE}
EOF
}
prepare_k3s_host_permissions() {
[[ "$MODE" == "k3s" ]] || return 0
local prep_pod="gitea-storage-perms"
kubectl -n "$NAMESPACE" delete pod "$prep_pod" --ignore-not-found >/dev/null 2>&1 || true
cat <<EOF | kubectl apply -n "$NAMESPACE" -f -
apiVersion: v1
kind: Pod
metadata:
name: ${prep_pod}
spec:
restartPolicy: Never
nodeSelector:
kubernetes.io/hostname: ${GITEA_PV_NODE}
containers:
- name: perms
image: busybox:1.36
command:
- /bin/sh
- -c
args:
- |
set -eu
mkdir -p \
/host/gitea/shared
chmod -R 0777 /host/gitea
securityContext:
runAsUser: 0
volumeMounts:
- name: host-root
mountPath: /host
volumes:
- name: host-root
hostPath:
path: ${GITEA_PV_BASE_DIR}
type: DirectoryOrCreate
EOF
kubectl -n "$NAMESPACE" wait --for=jsonpath='{.status.phase}'=Succeeded pod/${prep_pod} --timeout=120s >/dev/null
kubectl -n "$NAMESPACE" delete pod "$prep_pod" --ignore-not-found >/dev/null 2>&1 || true
}
reset_stuck_k3s_release() {
[[ "$MODE" == "k3s" ]] || return 0
local pending
pending=$(kubectl -n "$NAMESPACE" get pvc -o jsonpath='{range .items[*]}{.spec.storageClassName}:{.status.phase}{"\n"}{end}' 2>/dev/null | grep '^:Pending$' || true)
if [[ -z "$pending" ]]; then
return 0
fi
warn "Detected pending PVCs without storageClass in namespace ${NAMESPACE}; resetting stuck Gitea release"
if command -v helm >/dev/null 2>&1; then
helm uninstall "$RELEASE_NAME" -n "$NAMESPACE" >/dev/null 2>&1 || true
fi
kubectl -n "$NAMESPACE" delete pvc gitea-shared-storage data-gitea-postgresql-ha-postgresql-0 data-gitea-postgresql-ha-postgresql-1 data-gitea-postgresql-ha-postgresql-2 valkey-data-gitea-valkey-cluster-0 valkey-data-gitea-valkey-cluster-1 valkey-data-gitea-valkey-cluster-2 >/dev/null 2>&1 || true
}
release_exists() {
command -v helm >/dev/null 2>&1 && helm status "$RELEASE_NAME" -n "$NAMESPACE" >/dev/null 2>&1
}
gitea_workload_exists() {
kubectl -n "$NAMESPACE" get deploy "$RELEASE_NAME" >/dev/null 2>&1
}
apply_manifest_fallback() { apply_manifest_fallback() {
warn "Helm unavailable; applying fallback manifest" if release_exists || gitea_workload_exists; then
warn "Helm resources detected for release '$RELEASE_NAME'; skipping fallback manifest to avoid immutable selector/port conflicts"
return 0
fi
warn "Applying fallback manifest"
local node_selector_block=""
if [[ -n "$NODE_SELECTOR" ]]; then
node_selector_block=$(cat <<EOF
nodeSelector:
kubernetes.io/hostname: ${NODE_SELECTOR}
EOF
)
fi
cat <<EOF | kubectl apply -n "$NAMESPACE" -f - cat <<EOF | kubectl apply -n "$NAMESPACE" -f -
apiVersion: apps/v1 apiVersion: apps/v1
kind: Deployment kind: Deployment
@ -148,6 +417,7 @@ spec:
labels: labels:
app: gitea app: gitea
spec: spec:
${node_selector_block}
containers: containers:
- name: gitea - name: gitea
image: ${IMAGE_REPO}:${IMAGE_TAG} image: ${IMAGE_REPO}:${IMAGE_TAG}
@ -193,6 +463,10 @@ case "$ACTION" in
esac esac
log "Deploying Gitea to namespace '$NAMESPACE' (mode=$MODE)" log "Deploying Gitea to namespace '$NAMESPACE' (mode=$MODE)"
reset_stuck_k3s_release
ensure_k3s_storage_layout
prepare_k3s_host_permissions
setup_knoe_db_for_gitea
if deploy_with_helm; then if deploy_with_helm; then
log "Gitea deployed via Helm" log "Gitea deployed via Helm"
else else

View File

@ -59,6 +59,7 @@ KONG_IMAGE="${KONG_IMAGE:-kong:3.9}"
KONG_NAME="${KONG_NAME:-prole-svc-kong}" KONG_NAME="${KONG_NAME:-prole-svc-kong}"
KONG_PROXY_PORT="${KONG_PROXY_PORT:-8000}" KONG_PROXY_PORT="${KONG_PROXY_PORT:-8000}"
KONG_ADMIN_PORT="${KONG_ADMIN_PORT:-8001}" KONG_ADMIN_PORT="${KONG_ADMIN_PORT:-8001}"
KONG_GITEA_SSH_PORT="${KONG_GITEA_SSH_PORT:-3022}"
KONG_CONFIG_NAME="${KONG_CONFIG_NAME:-prole-svc-kong-config}" KONG_CONFIG_NAME="${KONG_CONFIG_NAME:-prole-svc-kong-config}"
# Public service entrypoint (single source of truth from prole.cfg via prole_cfg.sh) # Public service entrypoint (single source of truth from prole.cfg via prole_cfg.sh)
@ -83,6 +84,10 @@ DB_MANAGER_SERVICE="${DB_MANAGER_SERVICE:-knoe-db-manager}"
DB_MANAGER_PORT="${DB_MANAGER_PORT:-80}" DB_MANAGER_PORT="${DB_MANAGER_PORT:-80}"
PROLE_SERVICE_UPSTREAM_URL="${PROLE_SERVICE_UPSTREAM_URL:-http://prole-svc.knoe-db.svc.cluster.local:8080}" PROLE_SERVICE_UPSTREAM_URL="${PROLE_SERVICE_UPSTREAM_URL:-http://prole-svc.knoe-db.svc.cluster.local:8080}"
GRAFANA_UPSTREAM_URL="${GRAFANA_UPSTREAM_URL:-http://kps-grafana.monitoring.svc.cluster.local:80}" GRAFANA_UPSTREAM_URL="${GRAFANA_UPSTREAM_URL:-http://kps-grafana.monitoring.svc.cluster.local:80}"
GITEA_HOSTNAME="${GITEA_HOSTNAME:-${GITEA_DOMAIN:-git.prole.org}}"
GITEA_HTTP_UPSTREAM_URL="${GITEA_HTTP_UPSTREAM_URL:-http://gitea-http.gitea.svc.cluster.local:3000}"
GITEA_SSH_UPSTREAM_HOST="${GITEA_SSH_UPSTREAM_HOST:-gitea-ssh.gitea.svc.cluster.local}"
GITEA_SSH_UPSTREAM_PORT="${GITEA_SSH_UPSTREAM_PORT:-22}"
# SSO wiring knobs # SSO wiring knobs
PROLE_GRAFANA_SSO_ENABLED="${PROLE_GRAFANA_SSO_ENABLED:-0}" PROLE_GRAFANA_SSO_ENABLED="${PROLE_GRAFANA_SSO_ENABLED:-0}"
@ -165,6 +170,8 @@ services:
url: ${PROLE_SERVICE_UPSTREAM_URL} url: ${PROLE_SERVICE_UPSTREAM_URL}
routes: routes:
- name: prole-k3s-kubeconfig - name: prole-k3s-kubeconfig
hosts:
- ${SERVICE_HOSTNAME}
paths: paths:
- /k3s/kube_config.sh - /k3s/kube_config.sh
strip_path: false strip_path: false
@ -173,6 +180,8 @@ services:
url: http://${DB_MANAGER_SERVICE}.${NAMESPACE}.svc.cluster.local:${DB_MANAGER_PORT} url: http://${DB_MANAGER_SERVICE}.${NAMESPACE}.svc.cluster.local:${DB_MANAGER_PORT}
routes: routes:
- name: backup-route - name: backup-route
hosts:
- ${SERVICE_HOSTNAME}
paths: paths:
- /backup - /backup
strip_path: false strip_path: false
@ -196,6 +205,27 @@ services:
paths: paths:
- / - /
strip_path: false strip_path: false
- name: gitea-http
url: ${GITEA_HTTP_UPSTREAM_URL}
routes:
- name: gitea-root
hosts:
- ${GITEA_HOSTNAME}
paths:
- /
strip_path: false
- name: gitea-ssh
host: ${GITEA_SSH_UPSTREAM_HOST}
port: ${GITEA_SSH_UPSTREAM_PORT}
protocol: tcp
routes:
- name: gitea-ssh-tcp
protocols:
- tcp
destinations:
- port: ${KONG_GITEA_SSH_PORT}
KONGEOF KONGEOF
) )
@ -243,6 +273,7 @@ apply_service_ingress() {
fi fi
local auth_host="${AUTH_HOSTNAME:-}" local auth_host="${AUTH_HOSTNAME:-}"
local gitea_host="${GITEA_HOSTNAME:-}"
local tls_hosts_extra="" local tls_hosts_extra=""
local rules_extra="" local rules_extra=""
if [[ -n "$auth_host" && "$auth_host" != "$host" ]]; then if [[ -n "$auth_host" && "$auth_host" != "$host" ]]; then
@ -262,6 +293,23 @@ EOF
) )
fi fi
if [[ -n "$gitea_host" && "$gitea_host" != "$host" && "$gitea_host" != "$auth_host" ]]; then
tls_hosts_extra+=$'\n - '"${gitea_host}"
rules_extra+=$'\n'$(cat <<EOF
- host: ${gitea_host}
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: ${KONG_NAME}
port:
number: ${KONG_PROXY_PORT}
EOF
)
fi
echo "Applying service Ingress for host '${host}' -> ${KONG_NAME}:${KONG_PROXY_PORT} (namespace=${NAMESPACE}) ..." echo "Applying service Ingress for host '${host}' -> ${KONG_NAME}:${KONG_PROXY_PORT} (namespace=${NAMESPACE}) ..."
( (
tmp="$(mktemp)" tmp="$(mktemp)"

View File

@ -709,6 +709,20 @@ def _expand_cfg_value(val: str, variables: dict) -> str:
return result return result
def _preserve_cfg_expr_for_persistence(
raw_value: str | None,
expanded_value: str | None = None,
) -> str:
"""Prefer raw tokenized value (e.g. `${HOME}`) when persisting config."""
raw = "" if raw_value is None else str(raw_value).strip()
if "$" in raw:
return raw
if expanded_value is None:
return raw
return str(expanded_value).strip()
def _parse_bool(val: any, default: bool | None = False) -> bool | None: def _parse_bool(val: any, default: bool | None = False) -> bool | None:
if val is None: if val is None:
return default return default
@ -1202,3 +1216,28 @@ def load_dependencies(refresh: bool = True) -> list[dict]:
except Exception: except Exception:
dep["installed"] = False dep["installed"] = False
return deps return deps
def _parse_gcp_cfg(path) -> dict:
"""Parse flat tfvars-style file written by etc/config.py --provider gcp.
Handles: key = "value" and key = value
Skips lines starting with # or ;
Returns empty dict if file missing or unreadable.
"""
result = {}
try:
for raw in path.read_text().splitlines():
line = raw.strip()
if not line or line.startswith("#") or line.startswith(";"):
continue
if "=" not in line:
continue
k, _, v = line.partition("=")
key = k.strip()
val = v.strip().strip('"').strip("'")
if key:
result[key] = val
except Exception:
pass
return result

View File

@ -2326,6 +2326,7 @@ def _render_prole_cfg(
"Initialize Cluster", "Initialize Cluster",
"Dev Cluster (k3d)", "Dev Cluster (k3d)",
"Service Cluster (k3s)", "Service Cluster (k3s)",
"GCP",
"Prod Cluster (k8s)", "Prod Cluster (k8s)",
"Docker Build", "Docker Build",
"Initialization Scripts", "Initialization Scripts",

View File

@ -333,6 +333,9 @@ def canonical_yaml(config: KnoeProductionConfig) -> str:
def opentofu_vars(config: KnoeProductionConfig) -> dict[str, Any]: def opentofu_vars(config: KnoeProductionConfig) -> dict[str, Any]:
return { return {
# GCP identity (populated from conf/prod/gcp.cfg via installer)
"gcp_project_id": config.cloud.projectId,
"gcp_region": config.cloud.region,
"ecosystem_id": config.metadata.ecosystemId, "ecosystem_id": config.metadata.ecosystemId,
"environment": config.metadata.environment, "environment": config.metadata.environment,
"project_id": config.cloud.projectId, "project_id": config.cloud.projectId,

View File

@ -588,6 +588,36 @@ def _extract_inventory_from_log(log_text: str) -> dict | None:
return data if isinstance(data, dict) else None return data if isinstance(data, dict) else None
def cleanup_topology_collectors(
*,
kubectl_base_cmd: Sequence[str],
namespace: str = TOPOLOGY_NAMESPACE,
env: dict[str, str] | None = None,
collector_name: str | None = None,
) -> None:
selector = (
f"prole.io/topology-collector={collector_name}"
if collector_name
else "app.kubernetes.io/name=prole-topology-collector"
)
for kind in ("daemonset", "pod"):
_run_kubectl(
kubectl_base_cmd,
[
"-n",
namespace,
"delete",
kind,
"-l",
selector,
"--ignore-not-found=true",
"--wait=false",
],
timeout=15,
env=env,
)
def _merge_node( def _merge_node(
k8s_node: dict, k8s_node: dict,
inventory: dict | None, inventory: dict | None,
@ -1078,19 +1108,11 @@ def discover_cluster_topology(
_progress(f"Timed out waiting for node {node_name}") _progress(f"Timed out waiting for node {node_name}")
if collector_applied: if collector_applied:
_run_kubectl( cleanup_topology_collectors(
kubectl_base_cmd, kubectl_base_cmd=kubectl_base_cmd,
[ namespace=cfg.namespace,
"-n",
cfg.namespace,
"delete",
"daemonset",
collector_name,
"--ignore-not-found=true",
"--wait=false",
],
timeout=15,
env=env, env=env,
collector_name=collector_name,
) )
merged_nodes: list[NodeTopology] = [] merged_nodes: list[NodeTopology] = []

View File

@ -22,6 +22,8 @@ from knoe import screen as ui
from knoe.config import ( from knoe.config import (
_expand_cfg_value, _expand_cfg_value,
_collect_cfg_vars, _collect_cfg_vars,
_parse_gcp_cfg,
_preserve_cfg_expr_for_persistence,
) )
from knoe.core.controller import KnoeController from knoe.core.controller import KnoeController
from knoe.core.env import ( from knoe.core.env import (
@ -412,8 +414,10 @@ class KnoeInstaller(
self.service_namespace = tk.StringVar(value="default") self.service_namespace = tk.StringVar(value="default")
self.gitops_enabled = tk.BooleanVar(value=False) self.gitops_enabled = tk.BooleanVar(value=False)
self.gitops_namespace = tk.StringVar(value="gitea") self.gitops_namespace = tk.StringVar(value="gitea")
self.gitops_node_selector = tk.StringVar(value="")
self.argocd_enabled = tk.BooleanVar(value=False) self.argocd_enabled = tk.BooleanVar(value=False)
self.argocd_namespace = tk.StringVar(value="argocd") self.argocd_namespace = tk.StringVar(value="argocd")
self.argocd_node_selector = tk.StringVar(value="")
# Load existing docker_import_dir from prole.cfg # Load existing docker_import_dir from prole.cfg
try: try:
@ -458,6 +462,12 @@ class KnoeInstaller(
saved_argocd_ns = _expand_cfg_value( saved_argocd_ns = _expand_cfg_value(
cfg["Global"].get("ARGOCD_NAMESPACE", ""), cfg_vars cfg["Global"].get("ARGOCD_NAMESPACE", ""), cfg_vars
).strip() ).strip()
saved_gitops_node = _expand_cfg_value(
cfg["Global"].get("GITEA_NODE_SELECTOR", ""), cfg_vars
).strip()
saved_argocd_node = _expand_cfg_value(
cfg["Global"].get("ARGOCD_NODE_SELECTOR", ""), cfg_vars
).strip()
saved_gitops_ns = ( saved_gitops_ns = (
_expand_cfg_value( _expand_cfg_value(
cfg.get("GitOps", "GITOPS_NAMESPACE", fallback=""), cfg.get("GitOps", "GITOPS_NAMESPACE", fallback=""),
@ -547,6 +557,22 @@ class KnoeInstaller(
self.prole_cfg_data.setdefault("GitOps", {})[ self.prole_cfg_data.setdefault("GitOps", {})[
"NAMESPACE" "NAMESPACE"
] = saved_gitops_ns ] = saved_gitops_ns
if saved_gitops_node:
try:
self.gitops_node_selector.set(saved_gitops_node)
except Exception:
pass
self.prole_cfg_data.setdefault("Global", {})[
"GITEA_NODE_SELECTOR"
] = saved_gitops_node
if saved_argocd_node:
try:
self.argocd_node_selector.set(saved_argocd_node)
except Exception:
pass
self.prole_cfg_data.setdefault("Global", {})[
"ARGOCD_NODE_SELECTOR"
] = saved_argocd_node
if saved_argocd_enabled: if saved_argocd_enabled:
val = saved_argocd_enabled.lower() in ("true", "1", "yes") val = saved_argocd_enabled.lower() in ("true", "1", "yes")
try: try:
@ -575,6 +601,33 @@ class KnoeInstaller(
mon[k] = v mon[k] = v
if mon: if mon:
self.prole_cfg_data["Monitoring"] = mon self.prole_cfg_data["Monitoring"] = mon
if cfg.has_section("Prod Cluster (k8s)"):
prod_sec = {}
for k, v in cfg.items("Prod Cluster (k8s)"):
try:
prod_sec[k.upper()] = _expand_cfg_value(v, cfg_vars)
except Exception:
prod_sec[k.upper()] = v
if prod_sec:
self.prole_cfg_data["Prod Cluster (k8s)"].update(prod_sec)
gcp_cfg_path = conf_dir / "prod" / "gcp.cfg"
gcp_values = _parse_gcp_cfg(gcp_cfg_path)
if gcp_values:
self.prole_cfg_data.setdefault("GCP", {}).update(
{k.upper(): v for k, v in gcp_values.items()}
)
# Preserve raw shell/config expressions for persistence so paths
# like `${HOME}/...` remain portable across hosts.
for section in cfg.sections():
section_data = self.prole_cfg_data.setdefault(section, {})
for key, raw_value in cfg.items(section):
section_data[key] = _preserve_cfg_expr_for_persistence(
raw_value,
section_data.get(key, raw_value),
)
except Exception: except Exception:
pass pass
@ -638,6 +691,18 @@ class KnoeInstaller(
self.prod_artifacts_path = tk.StringVar( self.prod_artifacts_path = tk.StringVar(
value=str(PROJECT_ROOT / "data" / "staging") value=str(PROJECT_ROOT / "data" / "staging")
) )
saved_artifacts_dir = (
self.prole_cfg_data.get("Prod Cluster (k8s)", {})
.get("ARTIFACTS_DIR", "")
.strip()
)
if saved_artifacts_dir:
try:
self.prod_artifacts_path.set(saved_artifacts_dir)
except Exception:
pass
# Stash GCP values so _ensure_prod_config_state can apply them lazily.
self._gcp_cfg_values = dict(self.prole_cfg_data.get("GCP", {}))
# GitOps provider choice (gitea / argocd / none) # GitOps provider choice (gitea / argocd / none)
self.gitops_provider = tk.StringVar(value="gitea") self.gitops_provider = tk.StringVar(value="gitea")
# Google Workspace # Google Workspace

View File

@ -92,7 +92,16 @@ class ArgoCDScreenMixin:
self._canvas_items.append(ns_window) self._canvas_items.append(ns_window)
self._overlay_widgets.append(ns_entry) self._overlay_widgets.append(ns_entry)
y = 360 node_selector_var = getattr(self, "argocd_node_selector", None)
if hasattr(self, "_render_k3s_node_selector") and node_selector_var is not None:
y = self._render_k3s_node_selector(
y=360,
target_var=node_selector_var,
title="Node selector:",
)
else:
y = 360
initial_status = "Ready" if self.argocd_enabled.get() else "Disabled" initial_status = "Ready" if self.argocd_enabled.get() else "Disabled"
self._argocd_status_var = tk.StringVar(value=initial_status) self._argocd_status_var = tk.StringVar(value=initial_status)
status_item = ui.canvas_text( status_item = ui.canvas_text(
@ -196,6 +205,11 @@ class ArgoCDScreenMixin:
else: else:
mode = "k3d" mode = "k3d"
args = ["--mode", mode, "--namespace", namespace, "update"] args = ["--mode", mode, "--namespace", namespace, "update"]
node_selector = (self.argocd_node_selector.get() or "").strip()
if mode == "k3s" and node_selector:
env["ARGOCD_NODE_SELECTOR"] = node_selector
env["NODE_SELECTOR"] = node_selector
self._argocd_console.write(f"Node selector: {node_selector}\n")
cfg_path = None cfg_path = None
try: try:

View File

@ -39,6 +39,122 @@ class ScreenBaseMixin:
self._overlay_widgets.append(console) self._overlay_widgets.append(console)
return console return console
def _is_k3s_mode_active(self):
"""Return True when current mode resolves to k3s/service cluster."""
mode = (os.environ.get("PROLE_MODE") or "").strip().lower()
if mode == "k3s":
return True
try:
return (self.cluster_env.get() or "").strip().lower() == "service"
except Exception:
return False
def _fetch_k3s_nodes(self):
"""Return a list of node names from kubectl get nodes in k3s mode."""
cmd = ["kubectl", "get", "nodes", "-o", "name"]
try:
proc = subprocess.run(
cmd,
check=True,
capture_output=True,
text=True,
timeout=20,
)
names = []
for line in (proc.stdout or "").splitlines():
line = (line or "").strip()
if not line:
continue
if line.startswith("node/"):
line = line.split("/", 1)[1]
names.append(line)
if not names:
raise ValueError("no nodes returned")
return names
except Exception:
fallback = []
for candidate in ("gandalf.prole.org", "myrddin.prole.org"):
if candidate not in fallback:
fallback.append(candidate)
return fallback
def _is_tainted_node(self, node_name):
"""Return True if the node has taints and should be visually disabled."""
cmd = [
"kubectl",
"get",
"node",
node_name,
"-o",
"jsonpath={.spec.taints[*].key}",
]
try:
proc = subprocess.run(
cmd,
check=True,
capture_output=True,
text=True,
timeout=15,
)
return bool((proc.stdout or "").strip())
except Exception:
return node_name == "myrddin.prole.org"
def _render_k3s_node_selector(
self,
*,
y,
target_var,
title="Node selector:",
x_label=48,
x_options=190,
option_spacing=175,
):
"""Render a reusable k3s node selector and return the next y offset."""
if not self._is_k3s_mode_active():
return y
item = ui.canvas_text(
self,
x_label,
y,
title,
fill="black",
font=("SF Pro Text", 12),
)
self._canvas_items.append(item)
selected = (target_var.get() or "").strip()
nodes = self._fetch_k3s_nodes()
x = x_options
y_radio = y - 2
for node in nodes:
disabled = self._is_tainted_node(node)
rb = tk.Radiobutton(
self.bg_canvas,
text=node,
variable=target_var,
value=node,
bg="white",
fg="#8c8c8c" if disabled else "black",
activebackground="white",
selectcolor="white",
state="disabled" if disabled else "normal",
font=("SF Pro Text", 10),
)
window = self.bg_canvas.create_window(x, y_radio, window=rb, anchor="nw")
self._canvas_items.append(window)
self._overlay_widgets.append(rb)
x += option_spacing
if (not selected or selected not in nodes or self._is_tainted_node(selected)) and nodes:
for node in nodes:
if not self._is_tainted_node(node):
target_var.set(node)
break
return y + 36
def safe_after(self, func, delay=0): def safe_after(self, func, delay=0):
"""Run a function in the main thread if the root window still exists.""" """Run a function in the main thread if the root window still exists."""
if not self.root or not self.root.winfo_exists(): if not self.root or not self.root.winfo_exists():

View File

@ -112,6 +112,8 @@ class ConfigMixin:
"SUPABASE_PV_NODE": (self.supabase_pv_node.get() or "").strip(), "SUPABASE_PV_NODE": (self.supabase_pv_node.get() or "").strip(),
"SUPABASE_PV_BASE": (self.supabase_pv_base_dir.get() or "").strip(), "SUPABASE_PV_BASE": (self.supabase_pv_base_dir.get() or "").strip(),
"SUPABASE_PV_BASE_DIR": (self.supabase_pv_base_dir.get() or "").strip(), "SUPABASE_PV_BASE_DIR": (self.supabase_pv_base_dir.get() or "").strip(),
"GITEA_NODE_SELECTOR": (self.gitops_node_selector.get() or "").strip(),
"ARGOCD_NODE_SELECTOR": (self.argocd_node_selector.get() or "").strip(),
"PROLE_OPENTOFU_URL": _default_opentofu_pipeline_url(), "PROLE_OPENTOFU_URL": _default_opentofu_pipeline_url(),
} }
if prole_home_override: if prole_home_override:
@ -216,6 +218,7 @@ class ConfigMixin:
"Initialize Cluster", "Initialize Cluster",
"Docker Build", "Docker Build",
"Initialization Scripts", "Initialization Scripts",
"GCP",
"Deployment", "Deployment",
"Install", "Install",
] ]
@ -533,9 +536,11 @@ class ConfigMixin:
# GitOps # GitOps
_set("gitops.namespace", _get_var(self.gitops_namespace, "gitea")) _set("gitops.namespace", _get_var(self.gitops_namespace, "gitea"))
_set("gitops.node_selector", _get_var(self.gitops_node_selector, ""))
# ArgoCD # ArgoCD
_set("argocd.namespace", _get_var(self.argocd_namespace, "argocd")) _set("argocd.namespace", _get_var(self.argocd_namespace, "argocd"))
_set("argocd.node_selector", _get_var(self.argocd_node_selector, ""))
# Disk selection (installer packaging) # Disk selection (installer packaging)
_set("disk_selection.disk_type", _get_var(self.selected_disk_type, "local")) _set("disk_selection.disk_type", _get_var(self.selected_disk_type, "local"))

View File

@ -18,6 +18,7 @@ from knoe.config import (
_encrypt_cfg_secret, _encrypt_cfg_secret,
_expand_path_expr, _expand_path_expr,
_merge_kubeconfig, _merge_kubeconfig,
_parse_gcp_cfg,
_write_k3s_kubeconfig, _write_k3s_kubeconfig,
) )
from knoe.core.actions import _reset_k3s_namespace from knoe.core.actions import _reset_k3s_namespace
@ -404,6 +405,35 @@ class ClusterScreenMixin:
) )
_add_row(cloud_tab, cloud_row, "DNS Zone", "cloud.dnsZone") _add_row(cloud_tab, cloud_row, "DNS Zone", "cloud.dnsZone")
def _on_load_gcp_cfg():
gcp_cfg_path = self._resolve_prole_conf_dir() / "prod" / "gcp.cfg"
loaded = _parse_gcp_cfg(gcp_cfg_path)
if not loaded:
import tkinter.messagebox as _mb
_mb.showwarning(
"GCP Setup",
f"No values found at:\n{gcp_cfg_path}\n\n"
"Run: python3 etc/config.py --mode k8s --provider gcp",
)
return
upper = {k.upper(): v for k, v in loaded.items()}
if upper.get("PROJECT_ID"):
self.prod_form_vars["cloud.projectId"].set(upper["PROJECT_ID"])
if upper.get("REGION"):
self.prod_form_vars["cloud.region"].set(upper["REGION"])
self.prole_cfg_data.setdefault("GCP", {}).update(upper)
self._gcp_cfg_values = dict(self.prole_cfg_data["GCP"])
load_gcp_btn = tk.Button(
cloud_tab,
text="Load from gcp.cfg",
command=_on_load_gcp_cfg,
font=("SF Pro Text", 10),
)
load_gcp_btn.grid(
row=cloud_row + 1, column=0, columnspan=2, sticky="w", padx=8, pady=(6, 2)
)
db_row = 0 db_row = 0
db_row = _add_row( db_row = _add_row(
db_tab, db_row, "Namespace", "kubernetes.namespace", readonly=True db_tab, db_row, "Namespace", "kubernetes.namespace", readonly=True
@ -1815,7 +1845,24 @@ class ClusterScreenMixin:
self._prod_preview_text_widget = None self._prod_preview_text_widget = None
self._prod_logs_text_widget = None self._prod_logs_text_widget = None
gcp = getattr(self, "_gcp_cfg_values", {}) or {}
if gcp.get("PROJECT_ID"):
self.prod_form_vars["cloud.projectId"].set(gcp["PROJECT_ID"])
if gcp.get("REGION") and self.prod_form_vars["cloud.region"].get() in ("", "us-central1"):
self.prod_form_vars["cloud.region"].set(gcp["REGION"])
try: try:
saved_yaml = (
(getattr(self, "prole_cfg_data", None) or {})
.get("Prod Cluster (k8s)", {})
.get("PRODUCTION_CONFIG_YAML", "")
.strip()
)
if saved_yaml:
import yaml as _yaml
saved_doc = _yaml.safe_load(saved_yaml) or {}
restored = build_config(saved_doc)
self.prod_config_api.put_prod_config(to_api_payload(restored))
payload = self.prod_config_api.get_prod_config() payload = self.prod_config_api.get_prod_config()
self._prod_apply_payload_to_vars(payload) self._prod_apply_payload_to_vars(payload)
except Exception: except Exception:
@ -2199,7 +2246,7 @@ class ClusterScreenMixin:
if env_key == "prod": if env_key == "prod":
self.prole_cfg_data["Prod Cluster (k8s)"][ self.prole_cfg_data["Prod Cluster (k8s)"][
"STAGING_DIRECTORY" "ARTIFACTS_DIR"
] = self.prod_artifacts_path.get() ] = self.prod_artifacts_path.get()
try: try:
prod_payload = self._prod_payload_from_vars() prod_payload = self._prod_payload_from_vars()

View File

@ -13,7 +13,7 @@ from knoe.core.env import PROJECT_ROOT, _normalize_cluster_env, _parse_bool
class GitOpsScreenMixin: class GitOpsScreenMixin:
"""GitOps / Gitea deployment screen.""" """GitOps deployment screen."""
# ------------------------------------------------------------------ # ------------------------------------------------------------------
# GitOps provider choice screen # GitOps provider choice screen
@ -120,7 +120,7 @@ class GitOpsScreenMixin:
anchor="ne", anchor="ne",
) )
self._render_title("GitOps / Gitea", y=150) self._render_title("GitOps", y=150)
description = ( description = (
"Deploys self-hosted GitOps (Gitea) via etc/init_gitea.sh.\n" "Deploys self-hosted GitOps (Gitea) via etc/init_gitea.sh.\n"
@ -171,7 +171,16 @@ class GitOpsScreenMixin:
self._canvas_items.append(ns_window) self._canvas_items.append(ns_window)
self._overlay_widgets.append(ns_entry) self._overlay_widgets.append(ns_entry)
y = 360 node_selector_var = getattr(self, "gitops_node_selector", None)
if hasattr(self, "_render_k3s_node_selector") and node_selector_var is not None:
y = self._render_k3s_node_selector(
y=360,
target_var=node_selector_var,
title="Node selector:",
)
else:
y = 360
initial_status = "Ready" if self.gitops_enabled.get() else "Disabled" initial_status = "Ready" if self.gitops_enabled.get() else "Disabled"
self._gitops_status_var = tk.StringVar(value=initial_status) self._gitops_status_var = tk.StringVar(value=initial_status)
status_item = ui.canvas_text( status_item = ui.canvas_text(
@ -275,6 +284,11 @@ class GitOpsScreenMixin:
else: else:
mode = "k3d" mode = "k3d"
args = ["--mode", mode, "--namespace", namespace] args = ["--mode", mode, "--namespace", namespace]
node_selector = (self.gitops_node_selector.get() or "").strip()
if mode == "k3s" and node_selector:
env["GITEA_NODE_SELECTOR"] = node_selector
env["NODE_SELECTOR"] = node_selector
self._gitops_console.write(f"Node selector: {node_selector}\n")
cfg_path = None cfg_path = None
try: try:

View File

@ -6,6 +6,7 @@ from pathlib import Path
import tkinter as tk import tkinter as tk
from tkinter import ttk, messagebox, filedialog from tkinter import ttk, messagebox, filedialog
from knoe import config as inst_config from knoe import config as inst_config
from knoe.core.topology import cleanup_topology_collectors
from knoe.core.env import get_resource_path, _deployment_mode_from_env from knoe.core.env import get_resource_path, _deployment_mode_from_env
@ -582,6 +583,13 @@ class NavigationMixin:
if hasattr(self, "_validate_and_save_cluster_nodes_policy"): if hasattr(self, "_validate_and_save_cluster_nodes_policy"):
if not self._validate_and_save_cluster_nodes_policy(policy): if not self._validate_and_save_cluster_nodes_policy(policy):
return return
try:
env_key = self._cluster_env_key()
cleanup_topology_collectors(
kubectl_base_cmd=self._kubectl_base_cmd(env_key),
)
except Exception:
pass
self.show_page("common_services") self.show_page("common_services")
return return

View File

@ -62,31 +62,39 @@ class SupabaseScreenMixin:
self._canvas_items.append(enable_window) self._canvas_items.append(enable_window)
self._overlay_widgets.append(enable_cb) self._overlay_widgets.append(enable_cb)
y = 280 node_selector_var = getattr(self, "supabase_pv_node", None)
node_label = ui.canvas_text( if hasattr(self, "_render_k3s_node_selector") and node_selector_var is not None:
self, y = self._render_k3s_node_selector(
48, y=280,
y, target_var=node_selector_var,
"Node selector:", title="Node selector:",
fill="black", )
font=("SF Pro Text", 12), else:
) y = 280
self._canvas_items.append(node_label) if y == 280:
node_entry = tk.Entry( node_label = ui.canvas_text(
self.bg_canvas, self,
textvariable=self.supabase_pv_node, 48,
font=("SF Pro Text", 11), y,
width=38, "Node selector:",
relief="solid", fill="black",
bd=1, font=("SF Pro Text", 12),
) )
node_entry_window = self.bg_canvas.create_window( self._canvas_items.append(node_label)
190, y - 2, window=node_entry, anchor="nw" node_entry = tk.Entry(
) self.bg_canvas,
self._canvas_items.append(node_entry_window) textvariable=self.supabase_pv_node,
self._overlay_widgets.append(node_entry) font=("SF Pro Text", 11),
width=38,
y += 36 relief="solid",
bd=1,
)
node_entry_window = self.bg_canvas.create_window(
190, y - 2, window=node_entry, anchor="nw"
)
self._canvas_items.append(node_entry_window)
self._overlay_widgets.append(node_entry)
y += 36
storage_label = ui.canvas_text( storage_label = ui.canvas_text(
self, self,
48, 48,

View File

@ -1 +1 @@
158 159

View File

@ -1,53 +1,59 @@
Network Discovery Summary: Network Discovery Summary:
Primary Router: 10.0.0.1 (eero_5d:50:f2) Primary Router: 10.0.0.1 (eero_5d:50:f2)
DNS Servers: 10.0.0.4, 10.0.0.5, 100.100.100.100 DNS Servers: 100.100.100.100, 10.0.0.4, 10.0.0.5
Detected Devices: Detected Devices:
- 10.0.0.35 [7e:19:9a:fe:9f:ed] (7e:19:9a:fe:9f:ed): Ports [22, 445, 5900, 11434, 88], Services: ['SSH', 'VNC', 'SMB/CIFS (Possible Windows/AD)', 'Ollama', 'Active Directory Related'] - 10.0.0.35 [7e:19:9a:fe:9f:ed] (7e:19:9a:fe:9f:ed): Ports [22, 445, 5900, 11434, 88], Services: ['SSH', 'VNC', 'SMB/CIFS (Possible Windows/AD)', 'Ollama', 'Active Directory Related']
- 10.0.0.203 [00:11:32:3b:2f:08] (Synology_3b:2f:08): Ports [22, 80, 443, 2049, 445], Services: ['SSH', 'NFS', 'Web Server', 'SMB/CIFS (Possible Windows/AD)'] - 10.0.0.33 [ec:b5:fa:b0:76:e4] (PhilipsLight_b0:76:e4): Ports [443], Services: ['Web Server']
- 10.0.0.35\ [7e:19:9a:fe:9f:ed] (7e:19:9a:fe:9f:ed): Ports [], Services: [] - 10.0.0.170 [b0:ee:7b:ca:73:99] (Roku_ca:73:99): Ports [], Services: []
- 10.0.0.3 [2c:cf:67:8c:2b:47] (RaspberryPi_8c:2b:47): Ports [22, 80, 2049, 445, 5900, 88, 389, 636], Services: ['SSH', 'NFS', 'VNC', 'Web Server', 'SMB/CIFS (Possible Windows/AD)', 'Active Directory Related']
- 10.0.0.1 [9c:57:bc:5d:50:f2] (eero_5d:50:f2): Ports [], Services: [] - 10.0.0.1 [9c:57:bc:5d:50:f2] (eero_5d:50:f2): Ports [], Services: []
- 10.0.0.4 [b8:27:eb:b3:6f:2b] (RaspberryPiF_b3:6f:2b): Ports [22, 53, 443], Services: ['DNS', 'SSH', 'Web Server'] - 10.0.0.3 [2c:cf:67:8c:2b:47] (RaspberryPi_8c:2b:47): Ports [22, 53, 80, 443, 2049, 5900, 88, 389, 636], Services: ['DNS', 'SSH', 'NFS', 'VNC', 'Web Server', 'Active Directory Related']
- 10.0.0.5 [b8:27:eb:88:3a:41] (RaspberryPiF_88:3a:41): Ports [22, 2049, 5900], Services: ['SSH', 'NFS', 'VNC'] - 10.0.0.5 [b8:27:eb:88:3a:41] (RaspberryPiF_88:3a:41): Ports [22, 2049], Services: ['SSH', 'NFS']
- 10.0.0.4 [b8:27:eb:b3:6f:2b] (RaspberryPiF_b3:6f:2b): Ports [22, 53, 2049], Services: ['DNS', 'SSH', 'NFS']
- 10.0.0.203 [00:11:32:3b:2f:08] (Synology_3b:2f:08): Ports [22, 80, 443, 2049, 445], Services: ['SSH', 'NFS', 'Web Server', 'SMB/CIFS (Possible Windows/AD)']
- 10.0.0.32 [48:a6:b8:a4:8e:cc] (Sonos_a4:8e:cc): Ports [], Services: [] - 10.0.0.32 [48:a6:b8:a4:8e:cc] (Sonos_a4:8e:cc): Ports [], Services: []
- 10.0.0.26 [c8:db:26:08:52:f8] (Logitech_08:52:f8): Ports [], Services: []
- 10.0.0.6 [2c:cf:67:45:8d:97] (RaspberryPi_45:8d:97): Ports [22, 443], Services: ['SSH', 'Web Server'] - 10.0.0.6 [2c:cf:67:45:8d:97] (RaspberryPi_45:8d:97): Ports [22, 443], Services: ['SSH', 'Web Server']
- 10.0.0.2 [dc:a4:ca:ea:1a:2f] (Apple_ea:1a:2f): Ports [], Services: [] - 10.0.0.2 [dc:a4:ca:ea:1a:2f] (Apple_ea:1a:2f): Ports [], Services: []
- 10.0.0.26 [c8:db:26:08:52:f8] (Logitech_08:52:f8): Ports [], Services: []
- 10.0.0.7 [2c:cf:67:ca:93:ce] (RaspberryPi_ca:93:ce): Ports [22], Services: ['SSH'] - 10.0.0.7 [2c:cf:67:ca:93:ce] (RaspberryPi_ca:93:ce): Ports [22], Services: ['SSH']
- 10.0.0.22 [2c:cf:67:45:8d:97] (RaspberryPi_45:8d:97): Ports [22], Services: ['SSH'] - 10.0.0.22 [2c:cf:67:45:8d:97] (RaspberryPi_45:8d:97): Ports [22, 443], Services: ['SSH', 'Web Server']
- 10.0.0.33 [ec:b5:fa:b0:76:e4] (PhilipsLight_b0:76:e4): Ports [443], Services: ['Web Server'] - 10.0.0.189 [00:17:88:a3:2f:cc] (PhilipsLight_a3:2f:cc): Ports [80, 443], Services: ['Web Server']
- 10.0.0.48 [0c:fe:45:53:f4:3c] (SonyInteract_53:f4:3c): Ports [], Services: [] - 10.0.0.204 [50:eb:f6:56:f3:26] (ASUSTekCOMPU_56:f3:26): Ports [22, 3389, 445, 5900, 11434], Services: ['SSH', 'VNC', 'SMB/CIFS (Possible Windows/AD)', 'RDP (Windows)', 'Ollama']
- 10.0.0.37 [54:07:7d:22:c0:b2] (Netgear_22:c0:b2): Ports [80], Services: ['Web Server'] - 10.0.0.37 [54:07:7d:22:c0:b2] (Netgear_22:c0:b2): Ports [80], Services: ['Web Server']
- 10.0.0.45 [f8:b4:6a:30:a4:1f] (HewlettPacka_30:a4:1f): Ports [80, 443], Services: ['Web Server']
- 10.0.0.46 [d4:f7:d5:40:ab:17] (SonyInteract_40:ab:17): Ports [], Services: [] - 10.0.0.46 [d4:f7:d5:40:ab:17] (SonyInteract_40:ab:17): Ports [], Services: []
- 10.0.0.52 [2c:cf:67:ca:93:ce] (RaspberryPi_ca:93:ce): Ports [], Services: [] - 10.0.0.48 [0c:fe:45:53:f4:3c] (SonyInteract_53:f4:3c): Ports [], Services: []
- 10.0.0.41 [b8:27:eb:57:10:d9] (RaspberryPiF_57:10:d9): Ports [], Services: [] - 10.0.0.45 [f8:b4:6a:30:a4:1f] (HewlettPacka_30:a4:1f): Ports [80, 443], Services: ['Web Server']
- 10.0.0.41 [b8:27:eb:57:10:d9] (RaspberryPiF_57:10:d9): Ports [22], Services: ['SSH']
- 10.0.0.52 [2c:cf:67:ca:93:ce] (RaspberryPi_ca:93:ce): Ports [22], Services: ['SSH']
- 10.0.0.205 [a8:20:66:28:12:e7] (Apple_28:12:e7): Ports [22, 53, 445, 5900, 88], Services: ['DNS', 'SSH', 'VNC', 'SMB/CIFS (Possible Windows/AD)', 'Active Directory Related']
- 10.0.0.35\ [7e:19:9a:fe:9f:ed] (7e:19:9a:fe:9f:ed): Ports [], Services: []
- 10.0.0.188 [00:16:6c:c5:1f:54] (SamsungElect_c5:1f:54): Ports [80, 443], Services: ['Web Server']
- 10.0.0.208 [a0:ad:9f:30:84:5f] (ASUSTekCOMPU_30:84:5f): Ports [], Services: []
- 10.0.0.113 [60:81:10:92:02:4e] (Apple_92:02:4e): Ports [], Services: []
- 10.0.0.58 [7e:46:74:2a:b0:b2] (7e:46:74:2a:b0:b2): Ports [], Services: []
- 10.0.0.155 [62:17:97:b3:06:a1] (62:17:97:b3:06:a1): Ports [], Services: []
- 10.0.0.206 [00:a0:de:a2:0b:ef] (Yamaha_a2:0b:ef): Ports [80], Services: ['Web Server']
- 10.0.0.106 [48:a6:b8:a7:50:60] (Sonos_a7:50:60): Ports [], Services: []
- 10.0.0.95 [b8:27:eb:dd:6f:14] (RaspberryPiF_dd:6f:14): Ports [22, 2049, 5900], Services: ['SSH', 'NFS', 'VNC']
- 10.0.0.73 [4c:a9:19:b3:12:f8] (TuyaSmart_b3:12:f8): Ports [], Services: [] - 10.0.0.73 [4c:a9:19:b3:12:f8] (TuyaSmart_b3:12:f8): Ports [], Services: []
- 10.0.0.100 [24:fc:e5:51:cf:74] (SamsungElect_51:cf:74): Ports [], Services: [] - 10.0.0.100 [24:fc:e5:51:cf:74] (SamsungElect_51:cf:74): Ports [], Services: []
- 10.0.0.95 [b8:27:eb:88:3a:41] (RaspberryPiF_88:3a:41): Ports [2049], Services: ['NFS'] - 10.0.0.94 [f0:20:ff:bf:9a:19] (Intel_bf:9a:19): Ports [], Services: []
- 10.0.0.106 [48:a6:b8:a7:50:60] (Sonos_a7:50:60): Ports [], Services: [] Note: Potential host for NVIDIA cards (PC Hardware vendor)
- 10.0.0.99 [20:28:bc:f1:dc:ba] (Visionscape_f1:dc:ba): Ports [], Services: [] - 10.0.0.117 [40:f5:20:1e:5e:91] (Espressif_1e:5e:91): Ports [80], Services: ['Web Server']
- 10.0.0.107 [b8:06:0d:b7:7c:56] (TuyaSmart_b7:7c:56): Ports [], Services: [] - 10.0.0.107 [b8:06:0d:b7:7c:56] (TuyaSmart_b7:7c:56): Ports [], Services: []
- 10.0.0.111 [ca:0b:6c:5d:0a:e5] (ca:0b:6c:5d:0a:e5): Ports [], Services: []
- 10.0.0.113 [60:81:10:92:02:4e] (Apple_92:02:4e): Ports [], Services: []
- 10.0.0.123 [3c:ef:8c:96:e3:3c] (ZhejiangDahu_96:e3:3c): Ports [80], Services: ['Web Server'] - 10.0.0.123 [3c:ef:8c:96:e3:3c] (ZhejiangDahu_96:e3:3c): Ports [80], Services: ['Web Server']
- 10.0.0.1\ [9c:57:bc:5d:50:f2] (eero_5d:50:f2): Ports [], Services: []
- 10.0.0.124 [9c:8e:cd:02:73:c4] (AmcrestTechn_02:73:c4): Ports [80], Services: ['Web Server'] - 10.0.0.124 [9c:8e:cd:02:73:c4] (AmcrestTechn_02:73:c4): Ports [80], Services: ['Web Server']
- 10.0.0.127 [58:55:ca:4a:60:6e] (Apple_4a:60:6e): Ports [22], Services: ['SSH'] - 10.0.0.127 [58:55:ca:4a:60:6e] (Apple_4a:60:6e): Ports [22], Services: ['SSH']
- 10.0.0.117 [40:f5:20:1e:5e:91] (Espressif_1e:5e:91): Ports [80], Services: ['Web Server']
- 10.0.0.125 [a0:60:32:04:73:ba] (AmcrestTechn_04:73:ba): Ports [], Services: []
- 10.0.0.130 [f8:bb:bf:a3:84:eb] (eero_a3:84:eb): Ports [53], Services: ['DNS'] - 10.0.0.130 [f8:bb:bf:a3:84:eb] (eero_a3:84:eb): Ports [53], Services: ['DNS']
- 10.0.0.143 [60:5f:8d:7a:aa:32] (eero_7a:aa:32): Ports [53], Services: ['DNS'] - 10.0.0.125 [a0:60:32:04:73:ba] (AmcrestTechn_04:73:ba): Ports [80], Services: ['Web Server']
- 10.0.0.205 [a8:20:66:28:12:e7] (Apple_28:12:e7): Ports [22, 53, 445, 5900, 88], Services: ['DNS', 'SSH', 'VNC', 'SMB/CIFS (Possible Windows/AD)', 'Active Directory Related'] - 10.0.0.143 [60:5f:8d:7a:aa:32] (eero_7a:aa:32): Ports [], Services: []
- 10.0.0.128 [a8:20:66:28:12:e7] (Apple_28:12:e7): Ports [], Services: [] - 10.0.0.128 [a8:20:66:28:12:e7] (Apple_28:12:e7): Ports [], Services: []
- 10.0.0.145 [7c:a6:b0:04:db:1c] (7c:a6:b0:04:db:1c): Ports [], Services: [] - 10.0.0.145 [7c:a6:b0:04:db:1c] (7c:a6:b0:04:db:1c): Ports [], Services: []
- 10.0.0.170 [b0:ee:7b:ca:73:99] (Roku_ca:73:99): Ports [], Services: [] - 10.0.0.160 [96:c3:fd:58:02:6c] (96:c3:fd:58:02:6c): Ports [], Services: []
- 10.0.0.180 [4c:e1:73:42:1a:be] (HuizhouDehon_2:1a:be): Ports [445], Services: ['SMB/CIFS (Possible Windows/AD)']
- 10.0.0.175 [28:80:88:e4:10:1a] (Netgear_e4:10:1a): Ports [80], Services: ['Web Server'] - 10.0.0.175 [28:80:88:e4:10:1a] (Netgear_e4:10:1a): Ports [80], Services: ['Web Server']
- 10.0.0.189 [00:17:88:a3:2f:cc] (PhilipsLight_a3:2f:cc): Ports [80, 443], Services: ['Web Server']
- 10.0.0.196 [60:5f:8d:88:08:f2] (eero_88:08:f2): Ports [53], Services: ['DNS'] - 10.0.0.196 [60:5f:8d:88:08:f2] (eero_88:08:f2): Ports [53], Services: ['DNS']
- 10.0.0.188 [00:16:6c:c5:1f:54] (SamsungElect_c5:1f:54): Ports [80, 443], Services: ['Web Server'] - 10.0.0.199 [7c:a6:b0:01:a7:33] (7c:a6:b0:01:a7:33): Ports [], Services: []
- 10.0.0.204 [50:eb:f6:56:f3:26] (ASUSTekCOMPU_56:f3:26): Ports [22, 3389, 11434], Services: ['SSH', 'RDP (Windows)', 'Ollama']
- 10.0.0.206 [00:a0:de:a2:0b:ef] (Yamaha_a2:0b:ef): Ports [80], Services: ['Web Server']
- 10.0.0.1\ [9c:57:bc:5d:50:f2] (eero_5d:50:f2): Ports [], Services: []
Ollama Instances found at: 10.0.0.35, 10.0.0.204 Ollama Instances found at: 10.0.0.35, 10.0.0.204
Ansible Inventory Summary: Ansible Inventory Summary:

View File

@ -1154,6 +1154,10 @@ setup_knoe_db_for_supabase() {
CREATE SCHEMA IF NOT EXISTS knoe AUTHORIZATION knoe; CREATE SCHEMA IF NOT EXISTS knoe AUTHORIZATION knoe;
CREATE SCHEMA IF NOT EXISTS auth AUTHORIZATION supabase_auth_admin; CREATE SCHEMA IF NOT EXISTS auth AUTHORIZATION supabase_auth_admin;
CREATE SCHEMA IF NOT EXISTS storage AUTHORIZATION supabase_storage_admin; CREATE SCHEMA IF NOT EXISTS storage AUTHORIZATION supabase_storage_admin;
-- Idempotent: if storage schema pre-existed with a different owner (e.g. postgres),
-- CREATE SCHEMA IF NOT EXISTS is a no-op and ownership is NOT transferred.
-- Explicitly grant so supabase_storage_admin can create its migration tables there.
GRANT USAGE, CREATE ON SCHEMA storage TO supabase_storage_admin;
CREATE SCHEMA IF NOT EXISTS graphql_public; CREATE SCHEMA IF NOT EXISTS graphql_public;
CREATE SCHEMA IF NOT EXISTS _realtime; CREATE SCHEMA IF NOT EXISTS _realtime;
ALTER SCHEMA _realtime OWNER TO postgres; ALTER SCHEMA _realtime OWNER TO postgres;
@ -1298,6 +1302,27 @@ helm_render_values() {
export DB_PASSWORD="$_live_pg_pw" export DB_PASSWORD="$_live_pg_pw"
fi fi
# Resolve Garage S3 credentials from the live k8s secret so the renderer
# always has concrete values regardless of prole.cfg state.
if [[ -z "${GARAGE_S3_KEY_ID:-}" ]]; then
local _live_garage_key_id
_live_garage_key_id=$(kubectl get secret knoe-db-barman-s3 \
-n "${DATABASE_NAMESPACE:-knoe-db}" \
-o jsonpath='{.data.ACCESS_KEY_ID}' 2>/dev/null | base64 --decode 2>/dev/null || true)
if [[ -n "$_live_garage_key_id" ]]; then
export GARAGE_S3_KEY_ID="$_live_garage_key_id"
fi
fi
if [[ -z "${GARAGE_S3_ACCESS_KEY:-}" ]]; then
local _live_garage_secret
_live_garage_secret=$(kubectl get secret knoe-db-barman-s3 \
-n "${DATABASE_NAMESPACE:-knoe-db}" \
-o jsonpath='{.data.SECRET_ACCESS_KEY}' 2>/dev/null | base64 --decode 2>/dev/null || true)
if [[ -n "$_live_garage_secret" ]]; then
export GARAGE_S3_ACCESS_KEY="$_live_garage_secret"
fi
fi
# Always regenerate — never reuse stale cached values from a previous deploy attempt # Always regenerate — never reuse stale cached values from a previous deploy attempt
rm -f "$PROJECT_ROOT/supabase/helm/generated/values.generated.json" rm -f "$PROJECT_ROOT/supabase/helm/generated/values.generated.json"

View File

@ -91,6 +91,7 @@ spec:
imagePullPolicy: {{ .Values.image.storage.pullPolicy }} imagePullPolicy: {{ .Values.image.storage.pullPolicy }}
env: env:
{{- $storageEnv := (.Values.environment.storage | default dict) }} {{- $storageEnv := (.Values.environment.storage | default dict) }}
{{- $storageBackend := (default (ternary "s3" "file" .Values.deployment.minio.enabled) (get $storageEnv "STORAGE_BACKEND")) }}
{{- range $key, $value := $storageEnv }} {{- range $key, $value := $storageEnv }}
{{- if and (ne $key "DB_HOST") (ne $key "DB_PORT") (ne $key "DB_SSL") }} {{- if and (ne $key "DB_HOST") (ne $key "DB_PORT") (ne $key "DB_SSL") }}
- name: {{ $key }} - name: {{ $key }}
@ -138,7 +139,7 @@ spec:
key: database key: database
{{- end }} {{- end }}
- name: DATABASE_URL - name: DATABASE_URL
value: $(DB_DRIVER)://$(DB_USER):$(DB_PASSWORD_ENC)@{{ include "supabase.database.host" . }}:{{ include "supabase.database.port" . }}/{{ include "supabase.database.name" . }}?search_path=auth&sslmode={{ include "supabase.database.ssl" . }} value: $(DB_DRIVER)://$(DB_USER):$(DB_PASSWORD_ENC)@{{ include "supabase.database.host" . }}:{{ include "supabase.database.port" . }}/{{ include "supabase.database.name" . }}?search_path=storage,public&sslmode={{ include "supabase.database.ssl" . }}
- name: PGRST_JWT_SECRET - name: PGRST_JWT_SECRET
valueFrom: valueFrom:
secretKeyRef: secretKeyRef:
@ -196,7 +197,7 @@ spec:
{{- if not (hasKey $storageEnv "STORAGE_BACKEND") }} {{- if not (hasKey $storageEnv "STORAGE_BACKEND") }}
- name: STORAGE_BACKEND - name: STORAGE_BACKEND
value: {{ if .Values.deployment.minio.enabled }}s3{{ else }}file{{ end }} value: {{ $storageBackend }}
{{- end }} {{- end }}
{{- if .Values.deployment.minio.enabled }} {{- if .Values.deployment.minio.enabled }}
@ -212,25 +213,27 @@ spec:
- name: GLOBAL_S3_FORCE_PATH_STYLE - name: GLOBAL_S3_FORCE_PATH_STYLE
value: "true" value: "true"
{{- end }} {{- end }}
{{- end }}
{{- if eq $storageBackend "s3" }}
- name: AWS_ACCESS_KEY_ID - name: AWS_ACCESS_KEY_ID
valueFrom: valueFrom:
secretKeyRef: secretKeyRef:
{{- if .Values.secret.minio.secretRef }} {{- if .Values.secret.s3.secretRef }}
name: {{ .Values.secret.minio.secretRef }} name: {{ .Values.secret.s3.secretRef }}
key: {{ .Values.secret.minio.secretRefKey.user | default "user" }} key: {{ .Values.secret.s3.secretRefKey.keyId | default "keyId" }}
{{- else }} {{- else }}
name: {{ include "supabase.secret.minio" . }} name: {{ include "supabase.secret.s3" . }}
key: user key: keyId
{{- end }} {{- end }}
- name: AWS_SECRET_ACCESS_KEY - name: AWS_SECRET_ACCESS_KEY
valueFrom: valueFrom:
secretKeyRef: secretKeyRef:
{{- if .Values.secret.s3.secretRef }} {{- if .Values.secret.s3.secretRef }}
name: {{ .Values.secret.s3.secretRef }} name: {{ .Values.secret.s3.secretRef }}
key: {{ .Values.secret.s3.secretRefKey.password | default "password" }} key: {{ .Values.secret.s3.secretRefKey.accessKey | default "accessKey" }}
{{- else }} {{- else }}
name: {{ include "supabase.secret.minio" . }} name: {{ include "supabase.secret.s3" . }}
key: password key: accessKey
{{- end }} {{- end }}
{{- end }} {{- end }}
{{- with .Values.deployment.storage.livenessProbe }} {{- with .Values.deployment.storage.livenessProbe }}

View File

@ -340,17 +340,62 @@ def _build_overlay(cfg: configparser.ConfigParser, args: argparse.Namespace) ->
if _is_placeholder(supabase_storage_class): if _is_placeholder(supabase_storage_class):
supabase_storage_class = "merlin-local-iscsi-d002" supabase_storage_class = "merlin-local-iscsi-d002"
storage_backend = _first(
os.environ.get("SUPABASE_STORAGE_BACKEND", ""),
os.environ.get("STORAGE_BACKEND", ""),
_cfg_get(cfg, "Supabase", "STORAGE_BACKEND"),
_cfg_get(cfg, "Global", "STORAGE_BACKEND"),
default="s3",
).strip().lower()
if _is_placeholder(storage_backend):
storage_backend = ""
global_s3_endpoint = _first(
os.environ.get("SUPABASE_GLOBAL_S3_ENDPOINT", ""),
os.environ.get("GLOBAL_S3_ENDPOINT", ""),
_cfg_get(cfg, "Supabase", "GLOBAL_S3_ENDPOINT"),
_cfg_get(cfg, "Global", "GLOBAL_S3_ENDPOINT"),
default="http://garage.knoe-system.svc.cluster.local:3900",
).strip()
if _is_placeholder(global_s3_endpoint):
global_s3_endpoint = ""
garage_s3_key_id = _first(
os.environ.get("GARAGE_S3_KEY_ID", ""),
_cfg_get(cfg, "Supabase", "GARAGE_S3_KEY_ID"),
_cfg_get(cfg, "Global", "GARAGE_S3_KEY_ID"),
default="",
).strip()
garage_s3_access_key = _first(
os.environ.get("GARAGE_S3_ACCESS_KEY", ""),
_cfg_get(cfg, "Supabase", "GARAGE_S3_ACCESS_KEY"),
_cfg_get(cfg, "Global", "GARAGE_S3_ACCESS_KEY"),
default="",
).strip()
if _is_placeholder(garage_s3_key_id):
garage_s3_key_id = ""
if _is_placeholder(garage_s3_access_key):
garage_s3_access_key = ""
use_garage_s3 = storage_backend == "s3" and "garage" in global_s3_endpoint.lower()
if use_garage_s3 and (not garage_s3_key_id or not garage_s3_access_key):
raise SystemExit(
"Garage S3 credentials are required for Supabase storage. "
"Set GARAGE_S3_KEY_ID and GARAGE_S3_ACCESS_KEY in [Supabase] or [Global], "
"or via environment variables."
)
overlay: dict[str, Any] = { overlay: dict[str, Any] = {
"nameOverride": "supabase", "nameOverride": "supabase",
"fullnameOverride": "supabase", "fullnameOverride": "supabase",
"deployment": { "deployment": {
"db": {"enabled": False}, "db": {"enabled": False},
"functions": {"enabled": True}, "functions": {"enabled": True, "fullnameOverride": "supabase-functions"},
"vector": {"enabled": True}, "vector": {"enabled": True, "fullnameOverride": "supabase-vector"},
"kong": {"enabled": True}, "kong": {"enabled": True, "fullnameOverride": "supabase-kong"},
"storage": {"enabled": True}, "storage": {"enabled": True, "fullnameOverride": "supabase-storage"},
"minio": {"enabled": True}, "minio": {"enabled": not use_garage_s3, "fullnameOverride": "supabase-minio"},
"imgproxy": {"enabled": True}, "imgproxy": {"enabled": True, "fullnameOverride": "supabase-imgproxy"},
# Explicit fullnameOverride per component strips the chart name # Explicit fullnameOverride per component strips the chart name
# from pod names (avoids 'supabase-prole-supabase-<component>'). # from pod names (avoids 'supabase-prole-supabase-<component>').
"analytics": {"enabled": analytics_enabled, "fullnameOverride": "supabase-analytics"}, "analytics": {"enabled": analytics_enabled, "fullnameOverride": "supabase-analytics"},
@ -385,6 +430,7 @@ def _build_overlay(cfg: configparser.ConfigParser, args: argparse.Namespace) ->
"secret": { "secret": {
"db": {"password": db_password, "database": db_name}, "db": {"password": db_password, "database": db_name},
"jwt": {"secret": jwt_secret, "anonKey": anon_key, "serviceKey": service_key}, "jwt": {"secret": jwt_secret, "anonKey": anon_key, "serviceKey": service_key},
"s3": {"keyId": garage_s3_key_id, "accessKey": garage_s3_access_key},
}, },
"ingress": { "ingress": {
"enabled": True, "enabled": True,
@ -468,7 +514,7 @@ def render(args: argparse.Namespace) -> None:
cmd = [ cmd = [
"helm", "helm",
"template", "template",
"prole-supabase", "supabase",
str(REPO_ROOT / "supabase" / "helm" / "prole-supabase"), str(REPO_ROOT / "supabase" / "helm" / "prole-supabase"),
"-n", "-n",
meta["supabase_namespace"], meta["supabase_namespace"],

View File

@ -16,6 +16,7 @@ from knoe.config import (
_parse_bool as cfg_parse_bool, _parse_bool as cfg_parse_bool,
_expand_path as cfg_expand_path, _expand_path as cfg_expand_path,
_expand_cfg_value as cfg_expand_cfg_value, _expand_cfg_value as cfg_expand_cfg_value,
_preserve_cfg_expr_for_persistence,
_collect_cfg_vars as cfg_collect_cfg_vars, _collect_cfg_vars as cfg_collect_cfg_vars,
_filter_cfg_values_for_persistence, _filter_cfg_values_for_persistence,
_normalize_cfg_value_for_persistence, _normalize_cfg_value_for_persistence,
@ -324,6 +325,20 @@ class TestExpandHelpers:
assert cfg_expand_cfg_value("", {}) == "" assert cfg_expand_cfg_value("", {}) == ""
assert cfg_expand_cfg_value(None, {}) is None assert cfg_expand_cfg_value(None, {}) is None
def test_preserve_cfg_expr_for_persistence_keeps_home_token(self):
value = _preserve_cfg_expr_for_persistence(
"${HOME}/dev/prole/data/staging",
"/Users/chrisfu/dev/prole/data/staging",
)
assert value == "${HOME}/dev/prole/data/staging"
def test_preserve_cfg_expr_for_persistence_uses_expanded_without_tokens(self):
value = _preserve_cfg_expr_for_persistence(
"/Users/chrisfu/dev/prole/data/staging",
"/Users/chrisfu/dev/prole/data/staging",
)
assert value == "/Users/chrisfu/dev/prole/data/staging"
# ===== _collect_cfg_vars ===== # ===== _collect_cfg_vars =====