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=...
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
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
@ -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
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
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
; 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.
[User]
@ -8,6 +8,7 @@
[Inputs]
; Screen-scoped inputs used for unattended replays (-S)
argocd.node_selector = gandalf.prole.org
build.deploy_env = Dev
build.run_build = false
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_LOGS = /Users/chrisfu/dev/prole/logs
env_setup.PROLE_SERVICE = /Users/chrisfu/dev/prole/etc
gitops.node_selector = gandalf.prole.org
init_cluster.argocd_enabled = false
init_cluster.at_rest_encryption_enabled = true
init_cluster.cluster_env = service
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_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.mode = k3s
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.run_deploy = true
init_db_build.run_build = true
init_password.cluster_name = ${CLUSTER_NAME}
init_password.db_host_port = 5432
init_password.db_namespace = ${DATABASE_NAMESPACE}
init_password.db_password = ${PROLE_SECRET:v1:OcWBTOiijqf-bfTT:6r-ioWdDRdkPNxmbEAQP2TYEPaycWasP}
init_password.db_password_confirm = ${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:iC68SlzGpoPl9nma:iQEsd-2BzbTXdxTnSldMjHmrFrij_hWJ}
init_password.db_username = root
init_password.generate_ssh_key = true
init_scripts.run_scripts = true
kerberos_config.enabled = true
kerberos_config.init_authority = false
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.test_connection = false
kerberos_config.test_connection = true
kerberos_config.user = administrator
network_scan.run = true
ollama_config.model =
@ -85,6 +94,7 @@ supabase_config.pv_node = gandalf.prole.org
[Global]
; Variables used by name in more than one place or assumed global scope
ARGOCD_NAMESPACE = argocd
ARGOCD_NODE_SELECTOR = gandalf.prole.org
CLUSTER_ENV = service
CLUSTER_NAME = knoe-db
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
DATABASE_NAMESPACE = knoe-db
DB_HOST_PORT = 5432
DB_PASSWORD = ${PROLE_SECRET:v1:M7Rt2Xp273X8ixb5:Rjys8A6SqmBv19DvPAvgihwJ0eJ8Xvda}
DB_PASSWORD = ${PROLE_SECRET:v1:t7NgRHfXTXH-mHcx:CRbKP9b6ccuxrTMMeJ742Q48_vegoUY4}
DEPLOYMENT_MODE = k3s
DEPLOYMENT_TARGET = prole-service-cluster
DOCKER_IMPORT_DIR =
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
KUBECONTEXT = knoe.dev.prole.org
OPENTOFU_URL = http://myrddin.prole.org:8080
OPTIONAL_WORKLOADS_MIN_READY_SCHEDULABLE_NODES = 2
PROLE_HOME = /Users/chrisfu/dev/prole
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
SUPABASE_PV_BASE = /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.
[Dependencies]
; No configuration values captured yet for this section.
STATUS = All installed
[Network]
AD_DC_HOST = myrddin.prole.org
@ -128,7 +145,21 @@ KDC_AUTO_DETECTED = 10.0.0.3
KERBEROS_AUTO_ENABLED = True
[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]
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
[Monitoring]
GRAFANA_ADMIN_PASSWORD = ${PROLE_SECRET:v1:st2uaIMwx55ftSPt:HjzyDuKpzGJyIDVzIKWD6YDzyoqZ4iJ9}
GRAFANA_ADMIN_PASSWORD = ${PROLE_SECRET:v1:L2BhAyHRKVei4cGM:-J83-HR_CpnPFpiB6NbOXKdmmagVW2qL}
[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_HOST = http://fairyland.prole.org:11434
@ -149,40 +192,47 @@ OLLAMA_SERVER_HOST = fairyland.prole.org
OLLAMA_SERVER_PORT = 11434
[Optional Features]
AT_REST_ENCRYPTION_ENABLED = True
GITOPS_ENABLED = False
KERBEROS_ENABLED = True
SUPABASE_ENABLED = False
ARGOCD_ENABLED = False
AT_REST_ENCRYPTION_ENABLED = true
GITOPS_ENABLED = true
GITOPS_PROVIDER = gitea
KERBEROS_ENABLED = true
SUPABASE_ENABLED = true
[GitOps]
; No configuration values captured yet for this section.
STATUS = Deployed
[Database Creation]
; No configuration values captured yet for this section.
DB_USER = root
[Initialize Cluster]
ENVIRONMENT = service
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)]
CLUSTER_ENV = dev
CLUSTER_ENV = k3d-knoe-dev-cluster
DISPLAY_NAME = knoe-dev-cluster
KUBECTL_CONTEXT = knoe.dev.prole.org
KUBECTL_CONTEXT = service
MODE = k3d
[Service Cluster (k3s)]
CLUSTER_ENV = prole-service-cluster
DISPLAY_NAME = prole-service-cluster
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
PIPELINE_URL = http://myrddin.prole.org:8080
[GCP]
; No configuration values captured yet for this section.
[Prod Cluster (k8s)]
ARTIFACTS_DIR = /Users/chrisfu/dev/prole/data/staging
ARTIFACTS_DIR =
CLUSTER_ENV = prole-prod-cluster
DISPLAY_NAME = prole-prod-cluster
MODE = k8s
PIPELINE_URL = http://myrddin.prole.org:8080
[Docker Build]
; No configuration values captured yet for this section.
@ -195,4 +245,4 @@ MODE = k3s
TARGET = prole-service-cluster
[Install]
; No configuration values captured yet for this section.
STATUS = Finished

View File

@ -66,3 +66,13 @@ spec:
name: kong
port:
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
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
url: http://knoe-db-manager:80
routes:
- name: backup-route
hosts:
- svc.prole.org
paths:
- /backup
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
- containerPort: 8001
name: admin
- containerPort: 3022
name: ssh-proxy
env:
- name: KONG_DATABASE
value: "off"
@ -32,6 +34,8 @@ spec:
value: "0.0.0.0:8000"
- name: KONG_ADMIN_LISTEN
value: "0.0.0.0:8001"
- name: KONG_STREAM_LISTEN
value: "0.0.0.0:3022 reuseport backlog=16384"
- name: KONG_DECLARATIVE_CONFIG
value: "/etc/kong/declarative/kong.yml"
volumeMounts:

View File

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

View File

@ -17,3 +17,13 @@ spec:
name: kong
port:
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_TAG="${GITEA_IMAGE_TAG:-1.22.3}"
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
if [[ "$FORCE" -eq 1 ]]; then
@ -119,20 +231,177 @@ deploy_with_helm() {
# Minimal config: NodePort or ClusterIP + Ingress depending on environment.
# 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" \
-n "$NAMESPACE" \
--set image.repository="$IMAGE_REPO" \
--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}" \
"${helm_args[@]}" \
--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() {
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 -
apiVersion: apps/v1
kind: Deployment
@ -148,6 +417,7 @@ spec:
labels:
app: gitea
spec:
${node_selector_block}
containers:
- name: gitea
image: ${IMAGE_REPO}:${IMAGE_TAG}
@ -193,6 +463,10 @@ case "$ACTION" in
esac
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
log "Gitea deployed via Helm"
else

View File

@ -59,6 +59,7 @@ KONG_IMAGE="${KONG_IMAGE:-kong:3.9}"
KONG_NAME="${KONG_NAME:-prole-svc-kong}"
KONG_PROXY_PORT="${KONG_PROXY_PORT:-8000}"
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}"
# 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}"
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}"
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
PROLE_GRAFANA_SSO_ENABLED="${PROLE_GRAFANA_SSO_ENABLED:-0}"
@ -165,6 +170,8 @@ services:
url: ${PROLE_SERVICE_UPSTREAM_URL}
routes:
- name: prole-k3s-kubeconfig
hosts:
- ${SERVICE_HOSTNAME}
paths:
- /k3s/kube_config.sh
strip_path: false
@ -173,6 +180,8 @@ services:
url: http://${DB_MANAGER_SERVICE}.${NAMESPACE}.svc.cluster.local:${DB_MANAGER_PORT}
routes:
- name: backup-route
hosts:
- ${SERVICE_HOSTNAME}
paths:
- /backup
strip_path: false
@ -196,6 +205,27 @@ services:
paths:
- /
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
)
@ -243,6 +273,7 @@ apply_service_ingress() {
fi
local auth_host="${AUTH_HOSTNAME:-}"
local gitea_host="${GITEA_HOSTNAME:-}"
local tls_hosts_extra=""
local rules_extra=""
if [[ -n "$auth_host" && "$auth_host" != "$host" ]]; then
@ -262,6 +293,23 @@ EOF
)
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}) ..."
(
tmp="$(mktemp)"

View File

@ -709,6 +709,20 @@ def _expand_cfg_value(val: str, variables: dict) -> str:
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:
if val is None:
return default
@ -1202,3 +1216,28 @@ def load_dependencies(refresh: bool = True) -> list[dict]:
except Exception:
dep["installed"] = False
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",
"Dev Cluster (k3d)",
"Service Cluster (k3s)",
"GCP",
"Prod Cluster (k8s)",
"Docker Build",
"Initialization Scripts",

View File

@ -333,6 +333,9 @@ def canonical_yaml(config: KnoeProductionConfig) -> str:
def opentofu_vars(config: KnoeProductionConfig) -> dict[str, Any]:
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,
"environment": config.metadata.environment,
"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
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(
k8s_node: dict,
inventory: dict | None,
@ -1078,19 +1108,11 @@ def discover_cluster_topology(
_progress(f"Timed out waiting for node {node_name}")
if collector_applied:
_run_kubectl(
kubectl_base_cmd,
[
"-n",
cfg.namespace,
"delete",
"daemonset",
collector_name,
"--ignore-not-found=true",
"--wait=false",
],
timeout=15,
cleanup_topology_collectors(
kubectl_base_cmd=kubectl_base_cmd,
namespace=cfg.namespace,
env=env,
collector_name=collector_name,
)
merged_nodes: list[NodeTopology] = []

View File

@ -22,6 +22,8 @@ from knoe import screen as ui
from knoe.config import (
_expand_cfg_value,
_collect_cfg_vars,
_parse_gcp_cfg,
_preserve_cfg_expr_for_persistence,
)
from knoe.core.controller import KnoeController
from knoe.core.env import (
@ -412,8 +414,10 @@ class KnoeInstaller(
self.service_namespace = tk.StringVar(value="default")
self.gitops_enabled = tk.BooleanVar(value=False)
self.gitops_namespace = tk.StringVar(value="gitea")
self.gitops_node_selector = tk.StringVar(value="")
self.argocd_enabled = tk.BooleanVar(value=False)
self.argocd_namespace = tk.StringVar(value="argocd")
self.argocd_node_selector = tk.StringVar(value="")
# Load existing docker_import_dir from prole.cfg
try:
@ -458,6 +462,12 @@ class KnoeInstaller(
saved_argocd_ns = _expand_cfg_value(
cfg["Global"].get("ARGOCD_NAMESPACE", ""), cfg_vars
).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 = (
_expand_cfg_value(
cfg.get("GitOps", "GITOPS_NAMESPACE", fallback=""),
@ -547,6 +557,22 @@ class KnoeInstaller(
self.prole_cfg_data.setdefault("GitOps", {})[
"NAMESPACE"
] = 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:
val = saved_argocd_enabled.lower() in ("true", "1", "yes")
try:
@ -575,6 +601,33 @@ class KnoeInstaller(
mon[k] = v
if 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:
pass
@ -638,6 +691,18 @@ class KnoeInstaller(
self.prod_artifacts_path = tk.StringVar(
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)
self.gitops_provider = tk.StringVar(value="gitea")
# Google Workspace

View File

@ -92,7 +92,16 @@ class ArgoCDScreenMixin:
self._canvas_items.append(ns_window)
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"
self._argocd_status_var = tk.StringVar(value=initial_status)
status_item = ui.canvas_text(
@ -196,6 +205,11 @@ class ArgoCDScreenMixin:
else:
mode = "k3d"
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
try:

View File

@ -39,6 +39,122 @@ class ScreenBaseMixin:
self._overlay_widgets.append(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):
"""Run a function in the main thread if the root window still 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_BASE": (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(),
}
if prole_home_override:
@ -216,6 +218,7 @@ class ConfigMixin:
"Initialize Cluster",
"Docker Build",
"Initialization Scripts",
"GCP",
"Deployment",
"Install",
]
@ -533,9 +536,11 @@ class ConfigMixin:
# GitOps
_set("gitops.namespace", _get_var(self.gitops_namespace, "gitea"))
_set("gitops.node_selector", _get_var(self.gitops_node_selector, ""))
# ArgoCD
_set("argocd.namespace", _get_var(self.argocd_namespace, "argocd"))
_set("argocd.node_selector", _get_var(self.argocd_node_selector, ""))
# Disk selection (installer packaging)
_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,
_expand_path_expr,
_merge_kubeconfig,
_parse_gcp_cfg,
_write_k3s_kubeconfig,
)
from knoe.core.actions import _reset_k3s_namespace
@ -404,6 +405,35 @@ class ClusterScreenMixin:
)
_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 = _add_row(
db_tab, db_row, "Namespace", "kubernetes.namespace", readonly=True
@ -1815,7 +1845,24 @@ class ClusterScreenMixin:
self._prod_preview_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:
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()
self._prod_apply_payload_to_vars(payload)
except Exception:
@ -2199,7 +2246,7 @@ class ClusterScreenMixin:
if env_key == "prod":
self.prole_cfg_data["Prod Cluster (k8s)"][
"STAGING_DIRECTORY"
"ARTIFACTS_DIR"
] = self.prod_artifacts_path.get()
try:
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:
"""GitOps / Gitea deployment screen."""
"""GitOps deployment screen."""
# ------------------------------------------------------------------
# GitOps provider choice screen
@ -120,7 +120,7 @@ class GitOpsScreenMixin:
anchor="ne",
)
self._render_title("GitOps / Gitea", y=150)
self._render_title("GitOps", y=150)
description = (
"Deploys self-hosted GitOps (Gitea) via etc/init_gitea.sh.\n"
@ -171,7 +171,16 @@ class GitOpsScreenMixin:
self._canvas_items.append(ns_window)
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"
self._gitops_status_var = tk.StringVar(value=initial_status)
status_item = ui.canvas_text(
@ -275,6 +284,11 @@ class GitOpsScreenMixin:
else:
mode = "k3d"
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
try:

View File

@ -6,6 +6,7 @@ from pathlib import Path
import tkinter as tk
from tkinter import ttk, messagebox, filedialog
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
@ -582,6 +583,13 @@ class NavigationMixin:
if hasattr(self, "_validate_and_save_cluster_nodes_policy"):
if not self._validate_and_save_cluster_nodes_policy(policy):
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")
return

View File

@ -62,31 +62,39 @@ class SupabaseScreenMixin:
self._canvas_items.append(enable_window)
self._overlay_widgets.append(enable_cb)
y = 280
node_label = ui.canvas_text(
self,
48,
y,
"Node selector:",
fill="black",
font=("SF Pro Text", 12),
)
self._canvas_items.append(node_label)
node_entry = tk.Entry(
self.bg_canvas,
textvariable=self.supabase_pv_node,
font=("SF Pro Text", 11),
width=38,
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
node_selector_var = getattr(self, "supabase_pv_node", None)
if hasattr(self, "_render_k3s_node_selector") and node_selector_var is not None:
y = self._render_k3s_node_selector(
y=280,
target_var=node_selector_var,
title="Node selector:",
)
else:
y = 280
if y == 280:
node_label = ui.canvas_text(
self,
48,
y,
"Node selector:",
fill="black",
font=("SF Pro Text", 12),
)
self._canvas_items.append(node_label)
node_entry = tk.Entry(
self.bg_canvas,
textvariable=self.supabase_pv_node,
font=("SF Pro Text", 11),
width=38,
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(
self,
48,

View File

@ -1 +1 @@
158
159

View File

@ -1,53 +1,59 @@
Network Discovery Summary:
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:
- 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.35\ [7e:19:9a:fe:9f:ed] (7e:19:9a:fe:9f:ed): 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.33 [ec:b5:fa:b0:76:e4] (PhilipsLight_b0:76:e4): Ports [443], Services: ['Web Server']
- 10.0.0.170 [b0:ee:7b:ca:73:99] (Roku_ca:73:99): 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.5 [b8:27:eb:88:3a:41] (RaspberryPiF_88:3a:41): Ports [22, 2049, 5900], Services: ['SSH', 'NFS', 'VNC']
- 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], 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.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.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.22 [2c:cf:67:45:8d:97] (RaspberryPi_45:8d:97): Ports [22], Services: ['SSH']
- 10.0.0.33 [ec:b5:fa:b0:76:e4] (PhilipsLight_b0:76:e4): Ports [443], Services: ['Web Server']
- 10.0.0.48 [0c:fe:45:53:f4:3c] (SonyInteract_53:f4:3c): Ports [], Services: []
- 10.0.0.22 [2c:cf:67:45:8d:97] (RaspberryPi_45:8d:97): Ports [22, 443], Services: ['SSH', '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.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.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.52 [2c:cf:67:ca:93:ce] (RaspberryPi_ca:93:ce): Ports [], Services: []
- 10.0.0.41 [b8:27:eb:57:10:d9] (RaspberryPiF_57:10:d9): Ports [], Services: []
- 10.0.0.48 [0c:fe:45:53:f4:3c] (SonyInteract_53:f4:3c): 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.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.106 [48:a6:b8:a7:50:60] (Sonos_a7:50:60): Ports [], Services: []
- 10.0.0.99 [20:28:bc:f1:dc:ba] (Visionscape_f1:dc:ba): Ports [], Services: []
- 10.0.0.94 [f0:20:ff:bf:9a:19] (Intel_bf:9a:19): Ports [], Services: []
Note: Potential host for NVIDIA cards (PC Hardware vendor)
- 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.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.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.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.143 [60:5f:8d:7a:aa:32] (eero_7a:aa:32): Ports [53], Services: ['DNS']
- 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.125 [a0:60:32:04:73:ba] (AmcrestTechn_04:73:ba): Ports [80], Services: ['Web Server']
- 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.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.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.188 [00:16:6c:c5:1f:54] (SamsungElect_c5:1f:54): Ports [80, 443], Services: ['Web Server']
- 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: []
- 10.0.0.199 [7c:a6:b0:01:a7:33] (7c:a6:b0:01:a7:33): Ports [], Services: []
Ollama Instances found at: 10.0.0.35, 10.0.0.204
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 auth AUTHORIZATION supabase_auth_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 _realtime;
ALTER SCHEMA _realtime OWNER TO postgres;
@ -1298,6 +1302,27 @@ helm_render_values() {
export DB_PASSWORD="$_live_pg_pw"
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
rm -f "$PROJECT_ROOT/supabase/helm/generated/values.generated.json"

View File

@ -91,6 +91,7 @@ spec:
imagePullPolicy: {{ .Values.image.storage.pullPolicy }}
env:
{{- $storageEnv := (.Values.environment.storage | default dict) }}
{{- $storageBackend := (default (ternary "s3" "file" .Values.deployment.minio.enabled) (get $storageEnv "STORAGE_BACKEND")) }}
{{- range $key, $value := $storageEnv }}
{{- if and (ne $key "DB_HOST") (ne $key "DB_PORT") (ne $key "DB_SSL") }}
- name: {{ $key }}
@ -138,7 +139,7 @@ spec:
key: database
{{- end }}
- 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
valueFrom:
secretKeyRef:
@ -196,7 +197,7 @@ spec:
{{- if not (hasKey $storageEnv "STORAGE_BACKEND") }}
- name: STORAGE_BACKEND
value: {{ if .Values.deployment.minio.enabled }}s3{{ else }}file{{ end }}
value: {{ $storageBackend }}
{{- end }}
{{- if .Values.deployment.minio.enabled }}
@ -212,25 +213,27 @@ spec:
- name: GLOBAL_S3_FORCE_PATH_STYLE
value: "true"
{{- end }}
{{- end }}
{{- if eq $storageBackend "s3" }}
- name: AWS_ACCESS_KEY_ID
valueFrom:
secretKeyRef:
{{- if .Values.secret.minio.secretRef }}
name: {{ .Values.secret.minio.secretRef }}
key: {{ .Values.secret.minio.secretRefKey.user | default "user" }}
{{- if .Values.secret.s3.secretRef }}
name: {{ .Values.secret.s3.secretRef }}
key: {{ .Values.secret.s3.secretRefKey.keyId | default "keyId" }}
{{- else }}
name: {{ include "supabase.secret.minio" . }}
key: user
name: {{ include "supabase.secret.s3" . }}
key: keyId
{{- end }}
- name: AWS_SECRET_ACCESS_KEY
valueFrom:
secretKeyRef:
{{- if .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 }}
name: {{ include "supabase.secret.minio" . }}
key: password
name: {{ include "supabase.secret.s3" . }}
key: accessKey
{{- end }}
{{- end }}
{{- 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):
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] = {
"nameOverride": "supabase",
"fullnameOverride": "supabase",
"deployment": {
"db": {"enabled": False},
"functions": {"enabled": True},
"vector": {"enabled": True},
"kong": {"enabled": True},
"storage": {"enabled": True},
"minio": {"enabled": True},
"imgproxy": {"enabled": True},
"functions": {"enabled": True, "fullnameOverride": "supabase-functions"},
"vector": {"enabled": True, "fullnameOverride": "supabase-vector"},
"kong": {"enabled": True, "fullnameOverride": "supabase-kong"},
"storage": {"enabled": True, "fullnameOverride": "supabase-storage"},
"minio": {"enabled": not use_garage_s3, "fullnameOverride": "supabase-minio"},
"imgproxy": {"enabled": True, "fullnameOverride": "supabase-imgproxy"},
# Explicit fullnameOverride per component strips the chart name
# from pod names (avoids 'supabase-prole-supabase-<component>').
"analytics": {"enabled": analytics_enabled, "fullnameOverride": "supabase-analytics"},
@ -385,6 +430,7 @@ def _build_overlay(cfg: configparser.ConfigParser, args: argparse.Namespace) ->
"secret": {
"db": {"password": db_password, "database": db_name},
"jwt": {"secret": jwt_secret, "anonKey": anon_key, "serviceKey": service_key},
"s3": {"keyId": garage_s3_key_id, "accessKey": garage_s3_access_key},
},
"ingress": {
"enabled": True,
@ -468,7 +514,7 @@ def render(args: argparse.Namespace) -> None:
cmd = [
"helm",
"template",
"prole-supabase",
"supabase",
str(REPO_ROOT / "supabase" / "helm" / "prole-supabase"),
"-n",
meta["supabase_namespace"],

View File

@ -16,6 +16,7 @@ from knoe.config import (
_parse_bool as cfg_parse_bool,
_expand_path as cfg_expand_path,
_expand_cfg_value as cfg_expand_cfg_value,
_preserve_cfg_expr_for_persistence,
_collect_cfg_vars as cfg_collect_cfg_vars,
_filter_cfg_values_for_persistence,
_normalize_cfg_value_for_persistence,
@ -324,6 +325,20 @@ class TestExpandHelpers:
assert cfg_expand_cfg_value("", {}) == ""
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 =====