Checkpoint: rename installer to knoe + harden db build context

- Add build-context helper to copy Docker context safely (ignore runtime data, keep symlinks)

- Update UI and core actions to use ~/.prole/build and shared copy helper

- Add/adjust tests and scripts; introduce knoe ops helpers and update manifests

Co-authored-by: Junie <junie@jetbrains.com>
This commit is contained in:
chrisfu 2026-03-22 01:45:21 -07:00
parent 2244d6acd6
commit 4ee2b259c9
182 changed files with 3046 additions and 784 deletions

View File

@ -63,8 +63,8 @@ init:
@command -v tofu >/dev/null 2>&1 || (echo "Error: OpenTofu (tofu) not found in PATH." && exit 1) @command -v tofu >/dev/null 2>&1 || (echo "Error: OpenTofu (tofu) not found in PATH." && exit 1)
@echo "Syncing OpenTofu pipeline from $(PROLE_MODE) runtime into $(PIPELINE_DIR)..." @echo "Syncing OpenTofu pipeline from $(PROLE_MODE) runtime into $(PIPELINE_DIR)..."
@PROLE_MODE=$(PROLE_MODE) PROLE_CONF=$(PROLE_CONF) PROLE_GIT_REPO=$(DEPLOYMENT_REPO_URL) PYTHONPATH=$(CURDIR) $(PYTHON) - <<'PY' @PROLE_MODE=$(PROLE_MODE) PROLE_CONF=$(PROLE_CONF) PROLE_GIT_REPO=$(DEPLOYMENT_REPO_URL) PYTHONPATH=$(CURDIR) $(PYTHON) - <<'PY'
from installer.core.controller import ProleController from knoe.core.controller import ProleController
from installer.core.env import PROJECT_ROOT from knoe.core.env import PROJECT_ROOT
from prole.deployment import ProleDeployment from prole.deployment import ProleDeployment
import sys, os import sys, os
from pathlib import Path from pathlib import Path
@ -102,5 +102,5 @@ pyconv:
clean: clean:
@echo "Cleaning build artifacts..." @echo "Cleaning build artifacts..."
rm -rf $(BUILD_DIR) $(DIST_DIR) *.spec rm -rf $(BUILD_DIR) $(DIST_DIR) *.spec
rm -rf __pycache__ prole/__pycache__ installer/__pycache__ rm -rf __pycache__ prole/__pycache__ knoe/__pycache__
@echo "✓ Clean complete" @echo "✓ Clean complete"

View File

@ -1,10 +1,10 @@
; Prole Master Configuration File ; Prole Master Configuration File
; Generated by install.py on 2026-03-17 00:58:58 ; Generated by install.py on 2026-03-22 01:44:24
; 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]
; User-editable values; derived values below reference these by default. ; User-editable values; derived values below reference these by default.
; No configuration values captured yet for this section. ; No user values captured yet for this section.
[Inputs] [Inputs]
; Screen-scoped inputs used for unattended replays (-S) ; Screen-scoped inputs used for unattended replays (-S)
@ -37,51 +37,58 @@ dependencies.python.install = true
dependencies.verify_all = false dependencies.verify_all = false
disk_selection.disk_type = local disk_selection.disk_type = local
disk_selection.local_path = /Users/chrisfu/dev/prole/prole-tools-app/dist disk_selection.local_path = /Users/chrisfu/dev/prole/prole-tools-app/dist
disk_selection.removable_mount = disk_selection.removable_mount =
env_setup.NAMESPACE = ${NAMESPACE}
env_setup.PROLE_CONF = /Users/chrisfu/dev/prole/conf
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
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 = dev
init_cluster.deployment_target = prole-service-cluster init_cluster.deployment_target = prole-dev-cluster
init_cluster.gitops_enabled = false init_cluster.gitops_enabled = false
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:tOW53iZqoGRwqxUz:n0jktIyKBQ5cAggXfdR2oZ7OYyt859dRFHn5f4MzCy2zL7D_Ur9C-4e89RrTNPVLlGmd2GbHnP9uSLfnxRKe4zmypr5CfQnj3WDpwTr977EZBckkGFaSUQvUq-nShA-mRNDj14bXc2s5Oba9IQ9tiYRj4HT0W32MQ04HzQ==} init_cluster.k3s_token = ${PROLE_SECRET:v1:oJzjBIw61NsNXh7f:DJ9niYssxFCc6-gubQmWNlmQkhUhOtxLaXIIOTGpSxkfYK1uxcLw_BWb8n6kkl7uDkM1jEc4isjwjbycG5hRdd2at0oILHcyyFwrICH1roH4o0MHdd-kyxKkWo23WgSnTUGdRXfch5UWyPYZKniCxSbBhWud4ZFIZsZ4pw==}
init_cluster.kerberos_enabled = true init_cluster.kerberos_enabled = true
init_cluster.mode = k3s init_cluster.mode = k3d
init_cluster.start_cluster = true init_cluster.start_cluster = true
init_cluster.supabase_enabled = false init_cluster.supabase_enabled = false
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.db_password = init_password.db_host_port = 5432
init_password.db_password_confirm = init_password.db_namespace = ${NAMESPACE}
init_password.db_password = ${PROLE_SECRET:v1:8fVi1VEwmCDDXQ8X:lnf6j8OiIAe5hVp0gGKPc43S98G_GwM_}
init_password.db_password_confirm = ${PROLE_SECRET:v1:8fVi1VEwmCDDXQ8X:lnf6j8OiIAe5hVp0gGKPc43S98G_GwM_}
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.kdc = 10.0.0.3 kerberos_config.kdc = 10.0.0.3
; kerberos_config.password omitted (defaults to prompt/secret resolution) kerberos_config.password = ${PROLE_SECRET:v1:MYcGYgRYF_0uzmX4:tF8WMMCx7DOkEyeI2gdOBzZPuqrWF90C3VInrJsoPWGzxCys}
kerberos_config.realm = PROLE.ORG kerberos_config.realm = PROLE.ORG
kerberos_config.test_connection = false kerberos_config.test_connection = false
kerberos_config.user = administrator kerberos_config.user = administrator
network_scan.run = true network_scan.run = true
ollama_config.model = ollama_config.model =
ollama_config.server_host = ollama_config.server_host = fairyland.prole.org
ollama_config.server_port = 11434 ollama_config.server_port = 11434
[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
CLUSTER_ENV = service CLUSTER_ENV = dev
DB_HOST_PORT = 5432 DB_HOST_PORT = 5432
DB_PASSWORD = DB_PASSWORD = ${PROLE_SECRET:v1:dsPAIoTIWP8_8O35:RfNqyfh0j_eu8-tt9LZBQ_06wLa8D8aW}
DEPLOYMENT_MODE = k3s DEPLOYMENT_MODE = k3d
DEPLOYMENT_TARGET = prole-service-cluster DEPLOYMENT_TARGET = prole-dev-cluster
DOCKER_PRELOAD = false DOCKER_PRELOAD = false
KUBECONTEXT = prole-k3s
NAMESPACE = knoe-db NAMESPACE = knoe-db
OPTIONAL_WORKLOADS_MIN_READY_SCHEDULABLE_NODES = 2
PROLE_DB_USER = root PROLE_DB_USER = root
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:tOW53iZqoGRwqxUz:n0jktIyKBQ5cAggXfdR2oZ7OYyt859dRFHn5f4MzCy2zL7D_Ur9C-4e89RrTNPVLlGmd2GbHnP9uSLfnxRKe4zmypr5CfQnj3WDpwTr977EZBckkGFaSUQvUq-nShA-mRNDj14bXc2s5Oba9IQ9tiYRj4HT0W32MQ04HzQ==} PROLE_K3S_TOKEN = ${PROLE_SECRET:v1:oJzjBIw61NsNXh7f:DJ9niYssxFCc6-gubQmWNlmQkhUhOtxLaXIIOTGpSxkfYK1uxcLw_BWb8n6kkl7uDkM1jEc4isjwjbycG5hRdd2at0oILHcyyFwrICH1roH4o0MHdd-kyxKkWo23WgSnTUGdRXfch5UWyPYZKniCxSbBhWud4ZFIZsZ4pw==}
PROLE_OPENTOFU_URL = http://127.0.0.1:8080 PROLE_OPENTOFU_URL = http://127.0.0.1:8080
SERVICE_NAMESPACE = knoe-system SERVICE_NAMESPACE = knoe-system
@ -120,7 +127,10 @@ PROLE_SERVICE = /Users/chrisfu/dev/prole/etc
; No configuration values captured yet for this section. ; No configuration values captured yet for this section.
[Ollama] [Ollama]
; No configuration values captured yet for this section. OLLAMA_HOST = http://fairyland.prole.org:11434
OLLAMA_SERVERS = fairyland.prole.org:11434,k3d.localhost:11434,morgoth.prole.org:11434
OLLAMA_SERVER_HOST = fairyland.prole.org
OLLAMA_SERVER_PORT = 11434
[Optional Features] [Optional Features]
AT_REST_ENCRYPTION_ENABLED = True AT_REST_ENCRYPTION_ENABLED = True
@ -135,21 +145,21 @@ SUPABASE_ENABLED = False
; No configuration values captured yet for this section. ; No configuration values captured yet for this section.
[Initialize Cluster] [Initialize Cluster]
ENVIRONMENT = service ENVIRONMENT = dev
K3S_SERVER_URL = https://myrddin.prole.org:6443 K3S_SERVER_URL = https://myrddin.prole.org:6443
K3S_TOKEN = ${PROLE_SECRET:v1:tOW53iZqoGRwqxUz:n0jktIyKBQ5cAggXfdR2oZ7OYyt859dRFHn5f4MzCy2zL7D_Ur9C-4e89RrTNPVLlGmd2GbHnP9uSLfnxRKe4zmypr5CfQnj3WDpwTr977EZBckkGFaSUQvUq-nShA-mRNDj14bXc2s5Oba9IQ9tiYRj4HT0W32MQ04HzQ==} K3S_TOKEN = ${PROLE_SECRET:v1:oJzjBIw61NsNXh7f:DJ9niYssxFCc6-gubQmWNlmQkhUhOtxLaXIIOTGpSxkfYK1uxcLw_BWb8n6kkl7uDkM1jEc4isjwjbycG5hRdd2at0oILHcyyFwrICH1roH4o0MHdd-kyxKkWo23WgSnTUGdRXfch5UWyPYZKniCxSbBhWud4ZFIZsZ4pw==}
[Dev Cluster (k3d)] [Dev Cluster (k3d)]
CLUSTER_ENV = dev CLUSTER_ENV = dev
DISPLAY_NAME = knoe-dev-cluster DISPLAY_NAME = knoe-dev-cluster
KUBECTL_CONTEXT = prole-k3s KUBECTL_CONTEXT =
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:tOW53iZqoGRwqxUz:n0jktIyKBQ5cAggXfdR2oZ7OYyt859dRFHn5f4MzCy2zL7D_Ur9C-4e89RrTNPVLlGmd2GbHnP9uSLfnxRKe4zmypr5CfQnj3WDpwTr977EZBckkGFaSUQvUq-nShA-mRNDj14bXc2s5Oba9IQ9tiYRj4HT0W32MQ04HzQ==} K3S_TOKEN = ${PROLE_SECRET:v1:oJzjBIw61NsNXh7f:DJ9niYssxFCc6-gubQmWNlmQkhUhOtxLaXIIOTGpSxkfYK1uxcLw_BWb8n6kkl7uDkM1jEc4isjwjbycG5hRdd2at0oILHcyyFwrICH1roH4o0MHdd-kyxKkWo23WgSnTUGdRXfch5UWyPYZKniCxSbBhWud4ZFIZsZ4pw==}
MODE = k3s MODE = k3s
PIPELINE_URL = http://127.0.0.1:8080 PIPELINE_URL = http://127.0.0.1:8080
@ -161,14 +171,15 @@ MODE = k8s
PIPELINE_URL = http://127.0.0.1:8080 PIPELINE_URL = http://127.0.0.1:8080
[Docker Build] [Docker Build]
; No configuration values captured yet for this section. LOCAL_REGISTRY = localhost:5000
LOCAL_REGISTRY_INTERNAL = k3d-prole-registry.localhost:5000
[Initialization Scripts] [Initialization Scripts]
; No configuration values captured yet for this section. ; No configuration values captured yet for this section.
[Deployment] [Deployment]
MODE = k3s MODE = k3d
TARGET = prole-service-cluster TARGET = prole-dev-cluster
[Install] [Install]
; No configuration values captured yet for this section. ; No configuration values captured yet for this section.

View File

@ -1,3 +1,11 @@
# Port mappings for Prole Tools (generated). # Port mappings for Prole Tools (generated).
# 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
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=127.0.0.1
opentofu: local=8080 remote=8080 ns=knoe-system svc=opentofu address=0.0.0.0
dashboard: local=8443 remote=443 ns=kubernetes-dashboard svc=kubernetes-dashboard-kong-proxy address=127.0.0.1
postgres: local=5432 remote=5432 ns=knoe-db svc=prole-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

View File

@ -1 +1 @@
18 17

View File

@ -1,5 +1,5 @@
; Prole Master Configuration File ; Prole Master Configuration File
; Generated by install.py on 2026-03-20 16:59:08 ; Generated by install.py on 2026-03-21 23:40:10
; 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]
@ -50,7 +50,7 @@ init_cluster.cluster_env = prod
init_cluster.deployment_target = prole-prod-cluster init_cluster.deployment_target = prole-prod-cluster
init_cluster.gitops_enabled = false init_cluster.gitops_enabled = false
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:byngYfgT7BPkndc5:TukgM2QvPylhorfIm0t-tPysEPq-Lvc8lqbv594nzDsJxc6Kj6J9walkiVQS6o18ZJYNU7xpuYsRWV1yyZW8RBxvPi-glfs7C-sdj6Q3mwia2A9jLcxyJMck3_8z4L-mc7utmo8PIxw0MnjeF8P_Ixzoyi4cM3WoKtZiyA==} init_cluster.k3s_token = ${PROLE_SECRET:v1:iTRfvIPRo7v-aK5j:H6zki1L-02mHJQLXIjZyFnVWByy5TfoCASeGEi08tcfZCqqybxeCatiXK_NyADR1oElbaZnvjw83fuhircDNdRRY4sQo_hEuKU0ll9s9o6QZBKZZzWpgcU4WMX7YAS4aOgc8d5pBFUH0mC-ZcJ9lSWT3JxhgBXRdbVopQQ==}
init_cluster.kerberos_enabled = true init_cluster.kerberos_enabled = true
init_cluster.mode = k8s init_cluster.mode = k8s
init_cluster.start_cluster = true init_cluster.start_cluster = true
@ -67,7 +67,7 @@ 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.kdc = 10.0.0.3 kerberos_config.kdc = 10.0.0.3
kerberos_config.password = ${PROLE_SECRET:v1:A6qvhIB8BlPEcQjO:0tFeTyOFqqkn13rROLkgcJ9NgYmVYRD1ZjJzCAeFwfrgSxQx} kerberos_config.password = ${PROLE_SECRET:v1:rEiGEX6WGmmQ2vyE:EjwlefHMKjZwZRVIrh3x4VjqxnEddJ-EpRSaoqd3PjaZGTh5}
kerberos_config.realm = PROLE.ORG kerberos_config.realm = PROLE.ORG
kerberos_config.test_connection = false kerberos_config.test_connection = false
kerberos_config.user = administrator kerberos_config.user = administrator
@ -89,7 +89,7 @@ OPTIONAL_WORKLOADS_MIN_READY_SCHEDULABLE_NODES = 2
PROLE_DB_USER = root PROLE_DB_USER = root
PROLE_HOME = $HOME/dev/prole PROLE_HOME = $HOME/dev/prole
PROLE_K3S_SERVER = https://myrddin.prole.org:6443 PROLE_K3S_SERVER = https://myrddin.prole.org:6443
PROLE_K3S_TOKEN = ${PROLE_SECRET:v1:dQZZFvDfN_I8bFPY:yr9laHz55aM5lL_QTnBim1m0xBpX5SgfOPOVp-2HwLdii-2TNnT0gSAuWqSjgwI9JSoMT3QfD2lavVF2qMoWh9SFN8idZ3-VIYRsiZ7SM2BOiS_sbOG3_QDucvciuIxy5MXNyFEmxOGTtXwyzC1cnQjlsiEnKy0XbFF7dA==} PROLE_K3S_TOKEN = ${PROLE_SECRET:v1:cWc4c9mYxi20rxbR:wTCIMGahxg8rxxGotDtMhobeDIzf1k_9thQti2je2znzRVZVqV4TibePL21oghQvLLBbkjzd5hwIlUGGqq7sOYkpIetik4swjmaQnJ4DHGT82RWX3PZuuCrPWLGb5NOs4Hqv61TKYru5C-ImJzrPyEq7vJDgBG3aita0HQ==}
PROLE_OPENTOFU_URL = http://127.0.0.1:8080 PROLE_OPENTOFU_URL = http://127.0.0.1:8080
SERVICE_NAMESPACE = knoe-system SERVICE_NAMESPACE = knoe-system
@ -155,7 +155,7 @@ MODE = k3d
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:_iFv8sZdDHQxTxd4:a3RRUkKtO9ctIm_393LrEg9Lq9tN1Sfl7B3TZLnSwEXEKgpNnp9VAeicvFy8kV4unSX99yXZPhTf3RbTQ7qaTt4Oj8vU9aFK905qa5BVPOzmAZo_10mTcfXYTOeNXALl4sprStWambtJ4CEYol3XsIXFywzGq1jkldPyLw==} K3S_TOKEN = ${PROLE_SECRET:v1:3DHF_algPRkZXK_o:hMRZr17b7GihxuRs-6NPhlcS9h-aIctsZih2SS6dPCqwZabpKbG2ziZBgLiwOAhWih-ofvR8qbaOWdmbXurTYvWAWoj5F0MzOa35klQ6ujO2A2B2Y_uHrnzHXwkwEVufl9g8mucOkSMZ6UiVkNK4jYowVZHNTXe7uhghCA==}
MODE = k3s MODE = k3s
PIPELINE_URL = http://127.0.0.1:8080 PIPELINE_URL = http://127.0.0.1:8080

View File

@ -1,5 +1,5 @@
; Prole Master Configuration File ; Prole Master Configuration File
; Generated by install.py on 2026-03-20 17:05:22 ; Generated by install.py on 2026-03-21 13:37:28
; 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]
@ -50,7 +50,7 @@ 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 = false
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:s4MdUcnQRJBPdAss:irg5n342LjxLeLV8kGtoAAO_Od3QcmWtTXLQbPw2o2mUJrRnNEBOhN-z4Q6J3UZylEKp6ZhmD_TdRGqdpfpC79j1kFtaTPWCvl_cdHHiG9580AQ87V4XmtAwEh-_ICm4GS35szQHELlXkJH4j7CvfAngW8RW_1nvtLHQyQ==} init_cluster.k3s_token = ${PROLE_SECRET:v1:_r5tIPt5VZcr0SrV:e2_sHY49gG2i-vhQZr-42M1kekln4D4ZHq6tJrDRCn8f4KOVSn59rndLt4PuHmsebPSPP4tGHZV0MaVtdBMm4RJmk9IXbqV7BjSNruK2SXi62NOXIIUWZGNcVIOFjmv7EovfOifo3nye8142zKDspTeyo5Y2KY6qGLtS2g==}
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
@ -67,7 +67,7 @@ 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.kdc = 10.0.0.3 kerberos_config.kdc = 10.0.0.3
kerberos_config.password = ${PROLE_SECRET:v1:1zuEATgIOtkCPTIi:w2eKBUzce6mq6BJAlefU9cl6K05A9EDT81AQ1uSVzgI1NbUY} kerberos_config.password = ${PROLE_SECRET:v1:71ROfYyYysQDJDpX:0K4cB3p9nnMb1yi2OImqUm-pa9Zg21JYTmYHzYB_7czTKMQJ}
kerberos_config.realm = PROLE.ORG kerberos_config.realm = PROLE.ORG
kerberos_config.test_connection = false kerberos_config.test_connection = false
kerberos_config.user = administrator kerberos_config.user = administrator
@ -84,13 +84,12 @@ DB_PASSWORD =
DEPLOYMENT_MODE = k3s DEPLOYMENT_MODE = k3s
DEPLOYMENT_TARGET = prole-service-cluster DEPLOYMENT_TARGET = prole-service-cluster
DOCKER_PRELOAD = false DOCKER_PRELOAD = false
KUBECONTEXT = prole-k3s
NAMESPACE = prole-db NAMESPACE = prole-db
OPTIONAL_WORKLOADS_MIN_READY_SCHEDULABLE_NODES = 2 OPTIONAL_WORKLOADS_MIN_READY_SCHEDULABLE_NODES = 2
PROLE_DB_USER = root PROLE_DB_USER = root
PROLE_HOME = $HOME/dev/prole PROLE_HOME = $HOME/dev/prole
PROLE_K3S_SERVER = https://myrddin.prole.org:6443 PROLE_K3S_SERVER = https://myrddin.prole.org:6443
PROLE_K3S_TOKEN = ${PROLE_SECRET:v1:SNcp3N71DMccQw5G:UXmhfoiN8PKqw3jaarQxhhv5rtPjjUQTRSVo4pS6FkmHqLDt1xghL4RfgBdcaR5HA9lnlDc5jFpLruIrC1ivxk5HssTn1prde0lKvhioO7SkIOSl6HsA4XXosf6KveNASpxCdeZ6RkZlg2i7jpHxvxX3zbIrSKNISN5gEQ==} PROLE_K3S_TOKEN = ${PROLE_SECRET:v1:_r5tIPt5VZcr0SrV:e2_sHY49gG2i-vhQZr-42M1kekln4D4ZHq6tJrDRCn8f4KOVSn59rndLt4PuHmsebPSPP4tGHZV0MaVtdBMm4RJmk9IXbqV7BjSNruK2SXi62NOXIIUWZGNcVIOFjmv7EovfOifo3nye8142zKDspTeyo5Y2KY6qGLtS2g==}
PROLE_OPENTOFU_URL = http://127.0.0.1:8080 PROLE_OPENTOFU_URL = http://127.0.0.1:8080
SERVICE_NAMESPACE = knoe-system SERVICE_NAMESPACE = knoe-system
@ -135,10 +134,7 @@ OLLAMA_SERVER_HOST = fairyland.prole.org
OLLAMA_SERVER_PORT = 11434 OLLAMA_SERVER_PORT = 11434
[Optional Features] [Optional Features]
AT_REST_ENCRYPTION_ENABLED = True
GITOPS_ENABLED = False GITOPS_ENABLED = False
KERBEROS_ENABLED = True
SUPABASE_ENABLED = False
[GitOps] [GitOps]
; No configuration values captured yet for this section. ; No configuration values captured yet for this section.
@ -147,14 +143,12 @@ SUPABASE_ENABLED = False
; No configuration values captured yet for this section. ; No configuration values captured yet for this section.
[Initialize Cluster] [Initialize Cluster]
ENVIRONMENT = service ; No configuration values captured yet for this section.
K3S_SERVER_URL = https://myrddin.prole.org:6443
K3S_TOKEN = ${PROLE_SECRET:v1:570CIZjbSCH131v_:pDQwbH9nKYsCl5z0f-iAb-cilZlo1KLpI16c6Hgj0Ic81Jk1I7CSjEFo5UJ1ezV_pdat-7kxhaWE_TxCXG6ZnuOA8SBh3xCCi1LsgYFluPwm47b4IaS5ftvMOe5TVWbQfnzsXuU4SdO_3O9mgagKHdDIzu8kufYfpeDyqA==}
[Dev Cluster (k3d)] [Dev Cluster (k3d)]
CLUSTER_ENV = dev CLUSTER_ENV = dev
DISPLAY_NAME = knoe-dev-cluster DISPLAY_NAME = knoe-dev-cluster
KUBECTL_CONTEXT = prole-k3s KUBECTL_CONTEXT =
MODE = k3d MODE = k3d
[Service Cluster (k3s)] [Service Cluster (k3s)]

View File

@ -14,6 +14,7 @@ resources:
- prole-auth-deployment.yaml - prole-auth-deployment.yaml
- prole-auth-service.yaml - prole-auth-service.yaml
- prole-auth-kerberos-configmap.yaml - prole-auth-kerberos-configmap.yaml
- prole-kdc-configmap.yaml
- grafana-proxy-configmap.yaml - grafana-proxy-configmap.yaml
- grafana-proxy-deployment.yaml - grafana-proxy-deployment.yaml
- grafana-proxy-service.yaml - grafana-proxy-service.yaml

View File

@ -14,6 +14,119 @@ spec:
labels: labels:
app: prole-auth app: prole-auth
spec: spec:
initContainers:
- name: keytab-bootstrap
image: k3d-prole-registry:5000/prole-authority:latest
imagePullPolicy: IfNotPresent
command:
- /bin/bash
- -lc
- |
set -euo pipefail
export DEBIAN_FRONTEND=noninteractive
realm="${PROLE_KDC_REALM:-PROLE.ORG}"
admin_principal="${PROLE_KDC_ADMIN_PRINCIPAL:-admin/admin}"
if [[ "${admin_principal}" != *"@"* ]]; then
admin_principal="${admin_principal}@${realm}"
fi
svc_principal="${PROLE_KERBEROS_SERVICE_PRINCIPAL:?Missing PROLE_KERBEROS_SERVICE_PRINCIPAL}"
if [[ "${svc_principal}" != *"@"* ]]; then
svc_principal="${svc_principal}@${realm}"
fi
keytab_out="/etc/prole/keytabs/http.keytab"
mkdir -p "$(dirname "${keytab_out}")"
if [[ -f /mnt/keytab-secret/http.keytab ]]; then
cp /mnt/keytab-secret/http.keytab "${keytab_out}"
chmod 0400 "${keytab_out}" || true
exit 0
fi
if ! command -v kadmin.local >/dev/null 2>&1; then
echo "Installing Kerberos packages..."
echo "krb5-config krb5-config/default_realm string ${realm}" | debconf-set-selections || true
echo "krb5-config krb5-config/kerberos_servers string 127.0.0.1" | debconf-set-selections || true
echo "krb5-config krb5-config/admin_server string 127.0.0.1" | debconf-set-selections || true
apt-get update
apt-get install -y --no-install-recommends krb5-kdc krb5-admin-server krb5-user dnsutils ca-certificates
rm -rf /var/lib/apt/lists/*
fi
mkdir -p /etc/krb5kdc /var/lib/krb5kdc
if [[ -f /opt/prole-kdc/krb5.conf ]]; then
cp /opt/prole-kdc/krb5.conf /etc/krb5.conf
fi
if [[ -f /opt/prole-kdc/kdc.conf ]]; then
cp /opt/prole-kdc/kdc.conf /etc/krb5kdc/kdc.conf
fi
if [[ -f /opt/prole-kdc/kadm5.acl ]]; then
cp /opt/prole-kdc/kadm5.acl /etc/krb5kdc/kadm5.acl
fi
if [[ -z "${PROLE_KDC_MASTER_PASSWORD:-}" ]]; then
echo "ERROR: Missing required env PROLE_KDC_MASTER_PASSWORD (secret 'prole-kdc-secrets/master_password')." >&2
exit 1
fi
if [[ -z "${PROLE_KDC_ADMIN_PASSWORD:-}" ]]; then
echo "ERROR: Missing required env PROLE_KDC_ADMIN_PASSWORD (secret 'prole-kdc-secrets/admin_password')." >&2
exit 1
fi
if [[ ! -f /var/lib/krb5kdc/principal ]]; then
echo "Initializing realm database for ${realm}..."
kdb5_util create -s -r "${realm}" -P "${PROLE_KDC_MASTER_PASSWORD}"
fi
if ! kadmin.local -q "get_principal ${admin_principal}" >/dev/null 2>&1; then
echo "Creating admin principal ${admin_principal}..."
kadmin.local -q "addprinc -pw ${PROLE_KDC_ADMIN_PASSWORD} ${admin_principal}"
fi
if ! kadmin.local -q "get_principal ${svc_principal}" >/dev/null 2>&1; then
echo "Creating service principal ${svc_principal}..."
kadmin.local -q "addprinc -randkey ${svc_principal}"
fi
kadmin.local -q "ktadd -k ${keytab_out} -norandkey ${svc_principal}"
chmod 0400 "${keytab_out}" || true
env:
- name: PROLE_KDC_REALM
valueFrom:
configMapKeyRef:
name: prole-auth-kerberos
key: realm
- name: PROLE_KDC_ADMIN_PRINCIPAL
value: "admin/admin"
- name: PROLE_KDC_MASTER_PASSWORD
valueFrom:
secretKeyRef:
name: prole-kdc-secrets
key: master_password
- name: PROLE_KDC_ADMIN_PASSWORD
valueFrom:
secretKeyRef:
name: prole-kdc-secrets
key: admin_password
- name: PROLE_KERBEROS_SERVICE_PRINCIPAL
valueFrom:
configMapKeyRef:
name: prole-auth-kerberos
key: servicePrincipal
volumeMounts:
- name: keytab
mountPath: /etc/prole/keytabs
- name: keytab-secret
mountPath: /mnt/keytab-secret
readOnly: true
- name: prole-kdc-config
mountPath: /opt/prole-kdc
- name: prole-kdc-data
mountPath: /var/lib/krb5kdc
- name: prole-kdc-data
mountPath: /etc/krb5kdc
containers: containers:
- name: prole-auth - name: prole-auth
image: prole-auth:latest image: prole-auth:latest
@ -37,11 +150,74 @@ spec:
key: servicePrincipal key: servicePrincipal
- name: PROLE_KERBEROS_KEYTAB_PATH - name: PROLE_KERBEROS_KEYTAB_PATH
value: "/etc/prole/keytabs/http.keytab" value: "/etc/prole/keytabs/http.keytab"
- name: PROLE_KERBEROS_REALM
valueFrom:
configMapKeyRef:
name: prole-auth-kerberos
key: realm
volumeMounts: volumeMounts:
- name: keytab - name: keytab
mountPath: /etc/prole/keytabs mountPath: /etc/prole/keytabs
readOnly: true readOnly: true
- name: prole-kdc-config
mountPath: /etc/krb5.conf
subPath: krb5.conf
readOnly: true
- name: kdc
image: k3d-prole-registry:5000/prole-authority:latest
imagePullPolicy: IfNotPresent
command: ["/bin/bash", "/opt/prole-kdc/entrypoint.sh"]
env:
- name: PROLE_KDC_REALM
valueFrom:
configMapKeyRef:
name: prole-auth-kerberos
key: realm
- name: PROLE_KDC_ADMIN_PRINCIPAL
value: "admin/admin"
- name: PROLE_KDC_MASTER_PASSWORD
valueFrom:
secretKeyRef:
name: prole-kdc-secrets
key: master_password
- name: PROLE_KDC_ADMIN_PASSWORD
valueFrom:
secretKeyRef:
name: prole-kdc-secrets
key: admin_password
ports:
- name: krb5-udp
containerPort: 88
protocol: UDP
- name: krb5-tcp
containerPort: 88
protocol: TCP
- name: kpasswd-udp
containerPort: 464
protocol: UDP
- name: kpasswd-tcp
containerPort: 464
protocol: TCP
- name: kadmin
containerPort: 749
protocol: TCP
volumeMounts:
- name: prole-kdc-config
mountPath: /opt/prole-kdc
- name: prole-kdc-data
mountPath: /var/lib/krb5kdc
- name: prole-kdc-data
mountPath: /etc/krb5kdc
volumes: volumes:
- name: keytab - name: keytab
emptyDir: {}
- name: keytab-secret
secret: secret:
secretName: prole-auth-keytab secretName: prole-auth-keytab
optional: true
- name: prole-kdc-config
configMap:
name: prole-kdc-config
defaultMode: 0755
- name: prole-kdc-data
emptyDir: {}

View File

@ -5,3 +5,4 @@ metadata:
data: data:
# Kerberos HTTP service principal for SPNEGO (must match keytab) # Kerberos HTTP service principal for SPNEGO (must match keytab)
servicePrincipal: "HTTP/api.prole.org@PROLE.ORG" servicePrincipal: "HTTP/api.prole.org@PROLE.ORG"
realm: "PROLE.ORG"

View File

@ -11,4 +11,24 @@ spec:
- name: http - name: http
port: 8080 port: 8080
targetPort: http targetPort: http
- name: krb5-udp
port: 88
targetPort: 88
protocol: UDP
- name: krb5-tcp
port: 88
targetPort: 88
protocol: TCP
- name: kpasswd-udp
port: 464
targetPort: 464
protocol: UDP
- name: kpasswd-tcp
port: 464
targetPort: 464
protocol: TCP
- name: kadmin
port: 749
targetPort: 749
protocol: TCP
type: ClusterIP type: ClusterIP

View File

@ -4,7 +4,7 @@ metadata:
name: prole-db name: prole-db
spec: spec:
instances: 3 instances: 3
imageName: myrddin.prole.org:5000/prole-db:0.0.0 imageName: k3d-prole-registry:5000/knoe-db:17-137
postgresUID: 100 postgresUID: 100
postgresGID: 101 postgresGID: 101
maxSyncReplicas: 1 maxSyncReplicas: 1

View File

@ -0,0 +1,91 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: prole-kdc-config
data:
krb5.conf: |
[libdefaults]
default_realm = PROLE.ORG
dns_lookup_realm = false
dns_lookup_kdc = false
[realms]
PROLE.ORG = {
kdc = 127.0.0.1
admin_server = 127.0.0.1
}
kdc.conf: |
[kdcdefaults]
kdc_ports = 88
kdc_tcp_ports = 88
[realms]
PROLE.ORG = {
database_name = /var/lib/krb5kdc/principal
admin_keytab = FILE:/etc/krb5kdc/kadm5.keytab
acl_file = /etc/krb5kdc/kadm5.acl
key_stash_file = /etc/krb5kdc/stash
max_life = 10h 0m 0s
max_renewable_life = 7d 0h 0m 0s
default_principal_flags = +preauth
}
kadm5.acl: |
admin/admin@PROLE.ORG *
entrypoint.sh: |
#!/usr/bin/env bash
set -euo pipefail
export DEBIAN_FRONTEND=noninteractive
realm="${PROLE_KDC_REALM:-PROLE.ORG}"
admin_principal="${PROLE_KDC_ADMIN_PRINCIPAL:-admin/admin}"
if [[ "${admin_principal}" != *"@"* ]]; then
admin_principal="${admin_principal}@${realm}"
fi
if ! command -v krb5kdc >/dev/null 2>&1; then
echo "Installing Kerberos packages..."
echo "krb5-config krb5-config/default_realm string ${realm}" | debconf-set-selections || true
echo "krb5-config krb5-config/kerberos_servers string 127.0.0.1" | debconf-set-selections || true
echo "krb5-config krb5-config/admin_server string 127.0.0.1" | debconf-set-selections || true
apt-get update
apt-get install -y --no-install-recommends krb5-kdc krb5-admin-server krb5-user dnsutils ca-certificates
rm -rf /var/lib/apt/lists/*
fi
mkdir -p /etc/krb5kdc /var/lib/krb5kdc
if [[ -f /opt/prole-kdc/krb5.conf ]]; then
cp /opt/prole-kdc/krb5.conf /etc/krb5.conf
fi
if [[ -f /opt/prole-kdc/kdc.conf ]]; then
cp /opt/prole-kdc/kdc.conf /etc/krb5kdc/kdc.conf
fi
if [[ -f /opt/prole-kdc/kadm5.acl ]]; then
cp /opt/prole-kdc/kadm5.acl /etc/krb5kdc/kadm5.acl
fi
if [[ -z "${PROLE_KDC_MASTER_PASSWORD:-}" ]]; then
echo "ERROR: Missing required env PROLE_KDC_MASTER_PASSWORD (secret 'prole-kdc-secrets/master_password')." >&2
exit 1
fi
if [[ -z "${PROLE_KDC_ADMIN_PASSWORD:-}" ]]; then
echo "ERROR: Missing required env PROLE_KDC_ADMIN_PASSWORD (secret 'prole-kdc-secrets/admin_password')." >&2
exit 1
fi
if [[ ! -f /var/lib/krb5kdc/principal ]]; then
echo "Initializing realm database for ${realm}..."
kdb5_util create -s -r "${realm}" -P "${PROLE_KDC_MASTER_PASSWORD}"
fi
if ! kadmin.local -q "get_principal ${admin_principal}" >/dev/null 2>&1; then
echo "Creating admin principal ${admin_principal}..."
kadmin.local -q "addprinc -pw ${PROLE_KDC_ADMIN_PASSWORD} ${admin_principal}"
fi
echo "Starting krb5kdc and kadmind ..."
krb5kdc -n &
sleep 0.5
if ! pgrep -x krb5kdc >/dev/null 2>&1; then
echo "ERROR: krb5kdc failed to start. Check /var/log/ (syslog) for details." >&2
exit 1
fi
exec kadmind -nofork

View File

@ -0,0 +1,9 @@
apiVersion: v1
kind: Secret
metadata:
name: prole-kdc-secrets
type: Opaque
stringData:
# Strong random values (do not commit real secrets)
master_password: "CHANGE_ME"
admin_password: "CHANGE_ME"

View File

@ -36,12 +36,14 @@ This repos first-pass Grafana SSO is implemented as:
Manifests are under `deploy/opentofu/k3s/manifests/prole/`: Manifests are under `deploy/opentofu/k3s/manifests/prole/`:
- `prole-auth-deployment.yaml` / `prole-auth-service.yaml` - `prole-auth-deployment.yaml` / `prole-auth-service.yaml`
- `prole-kdc-configmap.yaml` (embedded KDC sidecar configuration)
- `grafana-proxy-configmap.yaml` / `grafana-proxy-deployment.yaml` / `grafana-proxy-service.yaml` - `grafana-proxy-configmap.yaml` / `grafana-proxy-deployment.yaml` / `grafana-proxy-service.yaml`
Required (provided externally): Required (provided externally):
- Secret `prole-auth-secrets` with key `sessionSecret` - Secret `prole-auth-secrets` with key `sessionSecret`
- Secret `prole-auth-keytab` containing the HTTP service keytab at `http.keytab` - Secret `prole-auth-keytab` containing the HTTP service keytab at `http.keytab`
- Secret `prole-kdc-secrets` with keys `master_password` and `admin_password` (for the embedded KDC)
#### Logout / session invalidation #### Logout / session invalidation

View File

@ -82,14 +82,14 @@ The ncurses implementation shares the same business logic (`ProleController`) as
**Key Components:** **Key Components:**
1. **installer/ncurses_ui.py** - UI primitives 1. **knoe/ncurses_ui.py** - UI primitives
- `CursesWindow` - Basic window wrapper with rendering helpers - `CursesWindow` - Basic window wrapper with rendering helpers
- `TerminalConsole` - Scrollable console output - `TerminalConsole` - Scrollable console output
- `NavFooter` - Navigation button bar - `NavFooter` - Navigation button bar
- `InputField` - Text input widget - `InputField` - Text input widget
- `Checkbox` - Checkbox widget - `Checkbox` - Checkbox widget
2. **installer/ncurses_installer.py** - Main installer class 2. **knoe/ncurses_installer.py** - Main installer class
- `ProleNcursesInstaller` - Screen management and rendering - `ProleNcursesInstaller` - Screen management and rendering
- `run_ncurses_installer()` - Entry point for ncurses mode - `run_ncurses_installer()` - Entry point for ncurses mode
@ -145,7 +145,7 @@ Test the ncurses interface:
```bash ```bash
# Verify modules load correctly # Verify modules load correctly
python3 -c "from installer.ncurses_installer import run_ncurses_installer; print('OK')" python3 -c "from knoe.ncurses_installer import run_ncurses_installer; print('OK')"
# Run the installer # Run the installer
./install.py --no-gui ./install.py --no-gui

View File

@ -2,7 +2,7 @@
set -euo pipefail set -euo pipefail
# etc/build_db.sh # etc/build_db.sh
# Purpose: Build prole-db image, supporting multi-platform (amd64, arm64) # Purpose: Build knoe-db image, supporting multi-platform (amd64, arm64)
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
@ -40,7 +40,7 @@ done
cd "$PROJECT_ROOT/prole-db" cd "$PROJECT_ROOT/prole-db"
if docker buildx version >/dev/null 2>&1; then if docker buildx version >/dev/null 2>&1; then
log "Building multi-platform image: prole-db:$TAG for $PLATFORMS" log "Building multi-platform image: knoe-db:$TAG for $PLATFORMS"
# Check if a builder exists that supports multi-platform # Check if a builder exists that supports multi-platform
if ! docker buildx inspect prole-builder >/dev/null 2>&1; then if ! docker buildx inspect prole-builder >/dev/null 2>&1; then
@ -54,15 +54,15 @@ if docker buildx version >/dev/null 2>&1; then
if [[ "$TAG" == */* ]]; then if [[ "$TAG" == */* ]]; then
BUILD_ARGS+=(-t "$TAG") BUILD_ARGS+=(-t "$TAG")
else else
BUILD_ARGS+=(-t "prole-db:$TAG") BUILD_ARGS+=(-t "knoe-db:$TAG")
fi fi
if [[ "$PUSH" == "true" ]]; then if [[ "$PUSH" == "true" ]]; then
log "Pushing to registry..." log "Pushing to registry..."
BUILD_ARGS+=(--push) BUILD_ARGS+=(--push)
# Also tag as prole-db:latest for local convenience if it's a remote tag # Also tag as knoe-db:latest for local convenience if it's a remote tag
if [[ "$TAG" == */* ]]; then if [[ "$TAG" == */* ]]; then
BUILD_ARGS+=(-t "prole-db:latest") BUILD_ARGS+=(-t "knoe-db:latest")
fi fi
else else
# If not pushing, we build only for local platform to allow --load # If not pushing, we build only for local platform to allow --load
@ -74,7 +74,7 @@ if docker buildx version >/dev/null 2>&1; then
if [[ "$TAG" == */* ]]; then if [[ "$TAG" == */* ]]; then
BUILD_ARGS+=(-t "$TAG") BUILD_ARGS+=(-t "$TAG")
else else
BUILD_ARGS+=(-t "prole-db:$TAG") BUILD_ARGS+=(-t "knoe-db:$TAG")
fi fi
BUILD_ARGS+=(--load) BUILD_ARGS+=(--load)
fi fi
@ -82,5 +82,5 @@ if docker buildx version >/dev/null 2>&1; then
docker buildx build --progress=plain "${BUILD_ARGS[@]}" . docker buildx build --progress=plain "${BUILD_ARGS[@]}" .
else else
log "docker buildx not found; building for local platform only" log "docker buildx not found; building for local platform only"
docker build --progress=plain -t "prole-db:$TAG" . docker build --progress=plain -t "knoe-db:$TAG" .
fi fi

View File

@ -506,7 +506,7 @@ get_latest_image() {
release=$(printf "%03d" "$release") release=$(printf "%03d" "$release")
fi fi
echo "prole-db:${pg_version}-${release}" echo "knoe-db:${pg_version}-${release}"
} }
resolve_cnpg_image() { resolve_cnpg_image() {
@ -585,7 +585,7 @@ resolve_cnpg_image() {
# If the image is unqualified (no registry), prefix it with the chosen registry # If the image is unqualified (no registry), prefix it with the chosen registry
if [[ "$image" != */* ]]; then if [[ "$image" != */* ]]; then
image="${registry}/${image}" image="${registry}/${image}"
# If the first path segment has no dot/colon, it's still unqualified (e.g., prole-db:TAG) # If the first path segment has no dot/colon, it's still unqualified (e.g., knoe-db:TAG)
elif [[ "$first" != *"."* && "$first" != *":"* ]]; then elif [[ "$first" != *"."* && "$first" != *":"* ]]; then
image="${registry}/${image}" image="${registry}/${image}"
fi fi
@ -1252,7 +1252,7 @@ _push_to_k3d_registry() {
local image="$1" local image="$1"
local cluster_name="$2" local cluster_name="$2"
local push_host="${LOCAL_REGISTRY:-localhost:5000}" local push_host="${LOCAL_REGISTRY:-localhost:5000}"
local plain_image="${image##*/}" # strip registry prefix, e.g. prole-db:18-088 local plain_image="${image##*/}" # strip registry prefix, e.g. knoe-db:18-088
if [[ -n "$push_host" ]]; then if [[ -n "$push_host" ]]; then
local push_ref="${push_host}/${plain_image}" local push_ref="${push_host}/${plain_image}"
@ -1272,7 +1272,7 @@ _push_to_k3d_registry() {
return 1 return 1
} }
# Pre-flight: ensure the prole-db image is available in the k3d cluster before # Pre-flight: ensure the knoe-db image is available in the k3d cluster before
# the CNPG operator ever tries to pull it, avoiding ErrImagePull backoff loops. # the CNPG operator ever tries to pull it, avoiding ErrImagePull backoff loops.
# Steps: containerd cache → Docker daemon (registry tag) → Docker daemon (plain tag) # Steps: containerd cache → Docker daemon (registry tag) → Docker daemon (plain tag)
# → tar import → docker build + push/import. # → tar import → docker build + push/import.
@ -1289,12 +1289,12 @@ _ensure_prole_db_image() {
if [[ "$VERSION" == "latest" || -z "$VERSION" ]]; then if [[ "$VERSION" == "latest" || -z "$VERSION" ]]; then
image=$(get_latest_image) image=$(get_latest_image)
else else
image="prole-db:$VERSION" image="knoe-db:$VERSION"
fi fi
fi fi
image=$(resolve_cnpg_image "$image") image=$(resolve_cnpg_image "$image")
local prole_db_dir="${PROLE_HOME:-$SCRIPT_DIR/..}/prole-db" local prole_db_dir="${PROLE_HOME:-$SCRIPT_DIR/..}/prole-db"
local plain_image="${image##*/}" # e.g. prole-db:18-088 local plain_image="${image##*/}" # e.g. knoe-db:18-088
if [[ "${PROLE_MODE:-}" == "k3s" ]]; then if [[ "${PROLE_MODE:-}" == "k3s" ]]; then
echo "Pre-flight: ensuring image '$image' is available in k3s registry/import path ..." echo "Pre-flight: ensuring image '$image' is available in k3s registry/import path ..."
@ -1380,7 +1380,7 @@ _ensure_prole_db_image() {
# Step 4: image not found anywhere — build from source then push + import # Step 4: image not found anywhere — build from source then push + import
if [[ ! -f "$prole_db_dir/Dockerfile" ]]; then if [[ ! -f "$prole_db_dir/Dockerfile" ]]; then
echo "ERROR: Dockerfile not found in '$prole_db_dir'; cannot build prole-db image." >&2 echo "ERROR: Dockerfile not found in '$prole_db_dir'; cannot build knoe-db image." >&2
return 1 return 1
fi fi
if [[ "${PROLE_MODE:-}" == "k3s" ]]; then if [[ "${PROLE_MODE:-}" == "k3s" ]]; then
@ -1388,7 +1388,7 @@ _ensure_prole_db_image() {
else else
echo " Image '$image' not found in k3d, Docker daemon, or docker-import dir." echo " Image '$image' not found in k3d, Docker daemon, or docker-import dir."
fi fi
echo " Building prole-db image from '$prole_db_dir' ..." echo " Building knoe-db image from '$prole_db_dir' ..."
if ! docker build -t "$plain_image" "$prole_db_dir"; then if ! docker build -t "$plain_image" "$prole_db_dir"; then
echo "ERROR: docker build failed for image '$plain_image'." >&2 echo "ERROR: docker build failed for image '$plain_image'." >&2
return 1 return 1
@ -1427,7 +1427,7 @@ ensure_prole_stack_resources() {
if [[ "$VERSION" == "latest" || -z "$VERSION" ]]; then if [[ "$VERSION" == "latest" || -z "$VERSION" ]]; then
image=$(get_latest_image) image=$(get_latest_image)
else else
image="prole-db:$VERSION" image="knoe-db:$VERSION"
fi fi
fi fi
image=$(resolve_cnpg_image "$image") image=$(resolve_cnpg_image "$image")
@ -2361,7 +2361,7 @@ restore_cnpg_cluster() {
if [[ "$VERSION" == "latest" || -z "$VERSION" ]]; then if [[ "$VERSION" == "latest" || -z "$VERSION" ]]; then
image=$(get_latest_image) image=$(get_latest_image)
else else
image="prole-db:$VERSION" image="knoe-db:$VERSION"
fi fi
image=$(resolve_cnpg_image "$image") image=$(resolve_cnpg_image "$image")
@ -2458,57 +2458,43 @@ bao_service_url() {
} }
fetch_admin_keys_and_db_pass_from_bao_or_local() { fetch_admin_keys_and_db_pass_from_bao_or_local() {
local token url local priv_b64 pub_b64
if [[ -f "$OPENBAO_TOKEN_FILE" ]]; then priv_b64=$(fetch_openbao_secret "$BAO_PATH_ADMIN" "admin_private_key_b64")
token=$(cat "$OPENBAO_TOKEN_FILE") pub_b64=$(fetch_openbao_secret "$BAO_PATH_ADMIN" "admin_public_key_b64")
else if [[ -n "${priv_b64:-}" && "${priv_b64:-}" != "null" && -n "${pub_b64:-}" && "${pub_b64:-}" != "null" ]]; then
token=""
fi
url=$(bao_service_url)
if [[ -n "$token" && -n "$url" ]]; then
echo "Attempting to read admin key pair from OpenBao kv/$BAO_PATH_ADMIN ..." echo "Attempting to read admin key pair from OpenBao kv/$BAO_PATH_ADMIN ..."
if curl -sS -H "X-Vault-Token: $token" "$url/v1/kv/data/$BAO_PATH_ADMIN" | jq -e '.data.data' >/dev/null 2>&1; then
local priv_b64 pub_b64
priv_b64=$(curl -sS -H "X-Vault-Token: $token" "$url/v1/kv/data/$BAO_PATH_ADMIN" | jq -r '.data.data.admin_private_key_b64')
pub_b64=$(curl -sS -H "X-Vault-Token: $token" "$url/v1/kv/data/$BAO_PATH_ADMIN" | jq -r '.data.data.admin_public_key_b64')
# Use a temporary file to determine where to save based on existing legacy or generic preference
local target_priv="$ADMIN_PRIV_GENERIC"
local target_pub="$ADMIN_PUB_GENERIC"
# If legacy keys exist, we might want to overwrite them too for compatibility
printf "%s" "$priv_b64" | base64 -d >"$target_priv"
printf "%s" "$pub_b64" | base64 -d >"$target_pub"
chmod 0600 "$target_priv"
# Mirror to legacy path if it was expected by other scripts
cp "$target_priv" "$ADMIN_PRIV_ED25519" 2>/dev/null || true
cp "$target_pub" "$ADMIN_PUB_ED25519" 2>/dev/null || true
fi
echo "Attempting to read database password from OpenBao kv/$BAO_PATH_DB ..." # Prefer the generic filenames.
if curl -sS -H "X-Vault-Token: $token" "$url/v1/kv/data/$BAO_PATH_DB" | jq -e '.data.data' >/dev/null 2>&1; then printf "%s" "$priv_b64" | base64 -d >"$ADMIN_PRIV_GENERIC"
local db_pass printf "%s" "$pub_b64" | base64 -d >"$ADMIN_PUB_GENERIC"
db_pass=$(curl -sS -H "X-Vault-Token: $token" "$url/v1/kv/data/$BAO_PATH_DB" | jq -r '.data.data.password') chmod 0600 "$ADMIN_PRIV_GENERIC"
if [[ -n "$db_pass" ]]; then
echo "Updating database user secret 'prole-db-user' from OpenBao ..."
kubectl create secret generic prole-db-user -n "$NAMESPACE" \
--from-literal=username=prole \
--from-literal=password="$db_pass" \
--dry-run=client -o yaml | kubectl_apply_retry "$NAMESPACE"
echo "Updating database superuser secret 'prole-db-superuser' from OpenBao ..." # Mirror to legacy path for compatibility.
kubectl create secret generic prole-db-superuser -n "$NAMESPACE" \ cp "$ADMIN_PRIV_GENERIC" "$ADMIN_PRIV_ED25519" 2>/dev/null || true
--from-literal=username=postgres \ cp "$ADMIN_PUB_GENERIC" "$ADMIN_PUB_ED25519" 2>/dev/null || true
--from-literal=password="$db_pass" \ fi
--dry-run=client -o yaml | kubectl_apply_retry "$NAMESPACE"
fi local db_pass db_user
fi db_pass=$(resolve_db_password)
db_user="${PROLE_DB_USER:-prole}"
if [[ -n "${db_pass:-}" && "${db_pass:-}" != "null" ]]; then
echo "Ensuring database user secret 'prole-db-user' ..."
kubectl create secret generic prole-db-user -n "$NAMESPACE" \
--from-literal=username="$db_user" \
--from-literal=password="$db_pass" \
--dry-run=client -o yaml | kubectl_apply_retry "$NAMESPACE"
echo "Ensuring database superuser secret 'prole-db-superuser' ..."
kubectl create secret generic prole-db-superuser -n "$NAMESPACE" \
--from-literal=username=postgres \
--from-literal=password="$db_pass" \
--dry-run=client -o yaml | kubectl_apply_retry "$NAMESPACE"
fi fi
# No env fallback: if OpenBao is unreachable and secrets are missing, fail clearly # No env fallback: if OpenBao is unreachable and secrets are missing, fail clearly
if ! kubectl -n "$NAMESPACE" get secret prole-db-user >/dev/null 2>&1; then if ! kubectl -n "$NAMESPACE" get secret prole-db-user >/dev/null 2>&1; then
echo "ERROR: 'prole-db-user' secret is missing in namespace '$NAMESPACE' and could not be resolved from OpenBao." >&2 echo "ERROR: 'prole-db-user' secret is missing in namespace '$NAMESPACE' and could not be resolved from OpenBao or local DB_PASSWORD." >&2
return 1 return 1
fi fi
@ -2783,7 +2769,7 @@ deploy_cluster() {
if [[ "$VERSION" == "latest" || -z "$VERSION" ]]; then if [[ "$VERSION" == "latest" || -z "$VERSION" ]]; then
image=$(get_latest_image) image=$(get_latest_image)
else else
image="prole-db:$VERSION" image="knoe-db:$VERSION"
fi fi
image=$(resolve_cnpg_image "$image") image=$(resolve_cnpg_image "$image")
sync_manifest_image "$image" sync_manifest_image "$image"

View File

@ -628,17 +628,23 @@ else
fi fi
if [[ "$ENABLE_KERBEROS" == "1" ]]; then if [[ "$ENABLE_KERBEROS" == "1" ]]; then
if [ -x "$SCRIPT_DIR/init_kdc.sh" ]; then # KDC is now embedded in the `prole-auth` pod (multi-container) by default.
kdc_action="$ACTION" # Only deploy a standalone KDC when explicitly requested.
case "$kdc_action" in if [[ "${PROLE_KDC_STANDALONE:-0}" == "1" ]]; then
stop) kdc_action="cleanup" ;; if [ -x "$SCRIPT_DIR/init_kdc.sh" ]; then
status) kdc_action="status" ;; kdc_action="$ACTION"
*) kdc_action="update" ;; case "$kdc_action" in
esac stop) kdc_action="cleanup" ;;
SERVICE_NAMESPACE="$NS" PROLE_KDC_NAMESPACE="$NS" \ status) kdc_action="status" ;;
"$SCRIPT_DIR/init_kdc.sh" "$kdc_action" || rc=$? *) kdc_action="update" ;;
esac
SERVICE_NAMESPACE="$NS" PROLE_KDC_NAMESPACE="$NS" \
"$SCRIPT_DIR/init_kdc.sh" "$kdc_action" || rc=$?
else
echo "WARN: init_kdc.sh not found; standalone KDC deploy skipped."
fi
else else
echo "WARN: init_kdc.sh not found; kerberos deploy skipped." echo "[INFO] Kerberos enabled: skipping standalone KDC deploy (KDC runs as `kdc` sidecar in `prole-auth`)."
fi fi
fi fi

View File

@ -26,6 +26,16 @@ K3S_REGISTRY_HOST=${K3S_REGISTRY_HOST:-$(registry_host_from_url "${PROLE_K3S_SER
K3S_REGISTRY_PORT=${K3S_REGISTRY_PORT:-5000} K3S_REGISTRY_PORT=${K3S_REGISTRY_PORT:-5000}
K3S_REGISTRY_NAMESPACE=${K3S_REGISTRY_NAMESPACE:-${REGISTRY_NAMESPACE:-${SERVICE_NAMESPACE:-${PROLE_NAMESPACE:-default}}}} K3S_REGISTRY_NAMESPACE=${K3S_REGISTRY_NAMESPACE:-${REGISTRY_NAMESPACE:-${SERVICE_NAMESPACE:-${PROLE_NAMESPACE:-default}}}}
K3S_REGISTRY_FILE=${K3S_REGISTRY_FILE:-/etc/rancher/k3s/registries.yaml} K3S_REGISTRY_FILE=${K3S_REGISTRY_FILE:-/etc/rancher/k3s/registries.yaml}
K3S_REGISTRY_SCHEME=${K3S_REGISTRY_SCHEME:-}
if [[ -z "${K3S_REGISTRY_SCHEME}" ]]; then
# k3d's local registry is plain HTTP by default.
if [[ "${K3S_REGISTRY_HOST}" == k3d-* ]]; then
K3S_REGISTRY_SCHEME="http"
else
K3S_REGISTRY_SCHEME="https"
fi
fi
ensure_root() { ensure_root() {
if [[ "$(id -u)" -ne 0 ]]; then if [[ "$(id -u)" -ne 0 ]]; then
@ -38,14 +48,28 @@ render_registries_yaml() {
local host="$1" local host="$1"
local port="$2" local port="$2"
local ns="$3" local ns="$3"
local scheme="$4"
if [[ "$scheme" == "http" ]]; then
cat <<EOF
mirrors:
"${host}:${port}":
endpoint:
- "http://${host}:${port}"
"registry.${ns}.svc.cluster.local:${port}":
endpoint:
- "http://${host}:${port}"
EOF
return 0
fi
cat <<EOF cat <<EOF
mirrors: mirrors:
"${host}:${port}": "${host}:${port}":
endpoint: endpoint:
- "https://${host}:${port}" - "${scheme}://${host}:${port}"
"registry.${ns}.svc.cluster.local:${port}": "registry.${ns}.svc.cluster.local:${port}":
endpoint: endpoint:
- "https://${host}:${port}" - "${scheme}://${host}:${port}"
configs: configs:
"${host}:${port}": "${host}:${port}":
tls: tls:
@ -64,8 +88,8 @@ case "$ACTION" in
fi fi
ensure_root ensure_root
mkdir -p "$(dirname "$K3S_REGISTRY_FILE")" mkdir -p "$(dirname "$K3S_REGISTRY_FILE")"
render_registries_yaml "$K3S_REGISTRY_HOST" "$K3S_REGISTRY_PORT" "$K3S_REGISTRY_NAMESPACE" >"$K3S_REGISTRY_FILE" render_registries_yaml "$K3S_REGISTRY_HOST" "$K3S_REGISTRY_PORT" "$K3S_REGISTRY_NAMESPACE" "$K3S_REGISTRY_SCHEME" >"$K3S_REGISTRY_FILE"
echo "Wrote $K3S_REGISTRY_FILE for ${K3S_REGISTRY_HOST}:${K3S_REGISTRY_PORT}" echo "Wrote $K3S_REGISTRY_FILE for ${K3S_REGISTRY_SCHEME}://${K3S_REGISTRY_HOST}:${K3S_REGISTRY_PORT}"
echo "Restart k3s to apply: sudo systemctl restart k3s" echo "Restart k3s to apply: sudo systemctl restart k3s"
;; ;;
status) status)

View File

@ -31,6 +31,9 @@ prole_ensure_kube_context || exit 1
ACTION=${1:-initialize} ACTION=${1:-initialize}
KDC_NAMESPACE=${PROLE_KDC_NAMESPACE:-${SERVICE_NAMESPACE:-${NAMESPACE:-default}}} KDC_NAMESPACE=${PROLE_KDC_NAMESPACE:-${SERVICE_NAMESPACE:-${NAMESPACE:-default}}}
# Namespace where prole-auth runs and expects the `prole-kdc-config` ConfigMap.
# Defaults to PROLE_NAMESPACE (from prole.cfg) when present.
PROLE_AUTH_NAMESPACE=${PROLE_AUTH_NAMESPACE:-${PROLE_NAMESPACE:-}}
PROLE_KDC_ENABLED=${PROLE_KDC_ENABLED:-1} PROLE_KDC_ENABLED=${PROLE_KDC_ENABLED:-1}
PROLE_KDC_NAME=${PROLE_KDC_NAME:-auth} PROLE_KDC_NAME=${PROLE_KDC_NAME:-auth}
PROLE_KDC_SERVICE=${PROLE_KDC_SERVICE:-auth} PROLE_KDC_SERVICE=${PROLE_KDC_SERVICE:-auth}
@ -810,6 +813,112 @@ EOF
apply_kdc_manifest apply_kdc_manifest
# prole-auth runs in PROLE_AUTH_NAMESPACE (default: PROLE_NAMESPACE) and mounts
# ConfigMap `prole-kdc-config` for its embedded KDC sidecar. When the KDC itself
# is deployed into a different namespace (default: SERVICE_NAMESPACE), ensure
# the configmap also exists in the prole-auth namespace to prevent FailedMount.
if [[ -n "${PROLE_AUTH_NAMESPACE:-}" && "${PROLE_AUTH_NAMESPACE}" != "${KDC_NAMESPACE}" ]]; then
if ! kubectl get namespace "$PROLE_AUTH_NAMESPACE" >/dev/null 2>&1; then
log "Creating namespace '$PROLE_AUTH_NAMESPACE' ..."
kubectl create namespace "$PROLE_AUTH_NAMESPACE" >/dev/null 2>&1 || true
fi
log "Ensuring ConfigMap 'prole-kdc-config' exists in namespace '${PROLE_AUTH_NAMESPACE}' for prole-auth ..."
cat <<EOF | kubectl apply -n "$PROLE_AUTH_NAMESPACE" -f -
apiVersion: v1
kind: ConfigMap
metadata:
name: prole-kdc-config
namespace: ${PROLE_AUTH_NAMESPACE}
data:
krb5.conf: |
[libdefaults]
default_realm = ${PROLE_KDC_REALM}
dns_lookup_realm = false
dns_lookup_kdc = false
[realms]
${PROLE_KDC_REALM} = {
kdc = 127.0.0.1
admin_server = 127.0.0.1
}${trust_block}
kdc.conf: |
[kdcdefaults]
kdc_ports = 88
kdc_tcp_ports = 88
[realms]
${PROLE_KDC_REALM} = {
database_name = /var/lib/krb5kdc/principal
admin_keytab = FILE:/etc/krb5kdc/kadm5.keytab
acl_file = /etc/krb5kdc/kadm5.acl
key_stash_file = /etc/krb5kdc/stash
max_life = 10h 0m 0s
max_renewable_life = 7d 0h 0m 0s
default_principal_flags = +preauth
}
kadm5.acl: |
${admin_acl_principal} *
entrypoint.sh: |
#!/usr/bin/env bash
set -euo pipefail
export DEBIAN_FRONTEND=noninteractive
realm="\${PROLE_KDC_REALM:-PROLE.ORG}"
admin_principal="\${PROLE_KDC_ADMIN_PRINCIPAL:-admin/admin}"
if [[ "\${admin_principal}" != *"@"* ]]; then
admin_principal="\${admin_principal}@\${realm}"
fi
if ! command -v krb5kdc >/dev/null 2>&1; then
echo "Installing Kerberos packages..."
echo "krb5-config krb5-config/default_realm string \${PROLE_KDC_REALM}" | debconf-set-selections || true
echo "krb5-config krb5-config/kerberos_servers string 127.0.0.1" | debconf-set-selections || true
echo "krb5-config krb5-config/admin_server string 127.0.0.1" | debconf-set-selections || true
apt-get update
apt-get install -y --no-install-recommends krb5-kdc krb5-admin-server krb5-user dnsutils ca-certificates
rm -rf /var/lib/apt/lists/*
fi
mkdir -p /etc/krb5kdc /var/lib/krb5kdc
if [[ -f /opt/prole-kdc/krb5.conf ]]; then
cp /opt/prole-kdc/krb5.conf /etc/krb5.conf
fi
if [[ -f /opt/prole-kdc/kdc.conf ]]; then
cp /opt/prole-kdc/kdc.conf /etc/krb5kdc/kdc.conf
fi
if [[ -f /opt/prole-kdc/kadm5.acl ]]; then
cp /opt/prole-kdc/kadm5.acl /etc/krb5kdc/kadm5.acl
fi
if [[ -z "\${PROLE_KDC_MASTER_PASSWORD:-}" ]]; then
echo "ERROR: Missing required env PROLE_KDC_MASTER_PASSWORD (secret 'prole-kdc-secrets/master_password')." >&2
exit 1
fi
if [[ -z "\${PROLE_KDC_ADMIN_PASSWORD:-}" ]]; then
echo "ERROR: Missing required env PROLE_KDC_ADMIN_PASSWORD (secret 'prole-kdc-secrets/admin_password')." >&2
exit 1
fi
if [[ ! -f /var/lib/krb5kdc/principal ]]; then
echo "Initializing realm database for \${realm}..."
kdb5_util create -s -r "\${realm}" -P "\${PROLE_KDC_MASTER_PASSWORD}"
fi
if ! kadmin.local -q "get_principal \${admin_principal}" >/dev/null 2>&1; then
echo "Creating admin principal \${admin_principal}..."
kadmin.local -q "addprinc -pw \${PROLE_KDC_ADMIN_PASSWORD} \${admin_principal}"
fi
echo "Starting krb5kdc and kadmind ..."
krb5kdc -n &
sleep 0.5
if ! pgrep -x krb5kdc >/dev/null 2>&1; then
echo "ERROR: krb5kdc failed to start. Check /var/log/ (syslog) for details." >&2
exit 1
fi
exec kadmind -nofork
EOF
fi
local rollout_timeout="$PROLE_KDC_ROLLOUT_TIMEOUT" local rollout_timeout="$PROLE_KDC_ROLLOUT_TIMEOUT"
if [[ "$deployment_present" -eq 0 ]]; then if [[ "$deployment_present" -eq 0 ]]; then
rollout_timeout="$PROLE_KDC_DEPLOY_TIMEOUT" rollout_timeout="$PROLE_KDC_DEPLOY_TIMEOUT"
@ -839,6 +948,10 @@ cleanup_prole_kdc() {
kubectl -n "$KDC_NAMESPACE" delete service "$PROLE_KDC_SERVICE" --ignore-not-found kubectl -n "$KDC_NAMESPACE" delete service "$PROLE_KDC_SERVICE" --ignore-not-found
kubectl -n "$KDC_NAMESPACE" delete deployment "$PROLE_KDC_NAME" --ignore-not-found kubectl -n "$KDC_NAMESPACE" delete deployment "$PROLE_KDC_NAME" --ignore-not-found
kubectl -n "$KDC_NAMESPACE" delete configmap prole-kdc-config --ignore-not-found kubectl -n "$KDC_NAMESPACE" delete configmap prole-kdc-config --ignore-not-found
if [[ -n "${PROLE_AUTH_NAMESPACE:-}" && "${PROLE_AUTH_NAMESPACE}" != "${KDC_NAMESPACE}" ]]; then
kubectl -n "$PROLE_AUTH_NAMESPACE" delete configmap prole-kdc-config --ignore-not-found
fi
} }
status() { status() {

View File

@ -481,6 +481,11 @@ default_port_forward_if_local() {
} }
sync_prole_kdc_trust() { sync_prole_kdc_trust() {
if [[ "${PROLE_KDC_STANDALONE:-0}" != "1" ]]; then
# Default deployment embeds the KDC as a sidecar in `prole-auth`; do not
# attempt to manage a standalone KDC unless explicitly requested.
return 0
fi
if [[ ! -x "$SCRIPT_DIR/init_kdc.sh" ]]; then if [[ ! -x "$SCRIPT_DIR/init_kdc.sh" ]]; then
return 0 return 0
fi fi

View File

@ -197,7 +197,7 @@ export_backup_bucket() {
local image pod_name export_subdir local image pod_name export_subdir
image=$(kubectl -n "$NAMESPACE" get cluster "$CNPG_CLUSTER_NAME" -o jsonpath='{.spec.imageName}' 2>/dev/null || true) image=$(kubectl -n "$NAMESPACE" get cluster "$CNPG_CLUSTER_NAME" -o jsonpath='{.spec.imageName}' 2>/dev/null || true)
image=${image:-${PROLE_DB_IMAGE:-prole-db:latest}} image=${image:-${PROLE_DB_IMAGE:-knoe-db:latest}}
pod_name="prole-barman-export-$(date +%s)" pod_name="prole-barman-export-$(date +%s)"
export_subdir="${BACKUP_DIR}/garage-export-$(date +%Y%m%d%H%M%S)" export_subdir="${BACKUP_DIR}/garage-export-$(date +%Y%m%d%H%M%S)"

View File

@ -241,8 +241,13 @@ deploy_service_layer() {
"$SCRIPT_DIR/init_kong.sh" -n "$ns" "$kong_action" || rc=$? "$SCRIPT_DIR/init_kong.sh" -n "$ns" "$kong_action" || rc=$?
if [[ "$ENABLE_KERBEROS" == "1" ]]; then if [[ "$ENABLE_KERBEROS" == "1" ]]; then
SERVICE_NAMESPACE="$ns" PROLE_KDC_NAMESPACE="$ns" \ # KDC is embedded in `prole-auth` by default. Only deploy standalone KDC when requested.
"$SCRIPT_DIR/init_kdc.sh" "$kdc_action" || rc=$? if [[ "${PROLE_KDC_STANDALONE:-0}" == "1" ]]; then
SERVICE_NAMESPACE="$ns" PROLE_KDC_NAMESPACE="$ns" \
"$SCRIPT_DIR/init_kdc.sh" "$kdc_action" || rc=$?
else
log "[INFO] Kerberos enabled: skipping standalone KDC deploy (KDC runs as `kdc` sidecar in `prole-auth`)."
fi
fi fi
return "$rc" return "$rc"
@ -265,7 +270,7 @@ cleanup_old_namespace() {
"$SCRIPT_DIR/init_garage_store.sh" stop || true "$SCRIPT_DIR/init_garage_store.sh" stop || true
KONG_NAMESPACE="$ns" SERVICE_NAMESPACE="$ns" \ KONG_NAMESPACE="$ns" SERVICE_NAMESPACE="$ns" \
"$SCRIPT_DIR/init_kong.sh" -n "$ns" stop || true "$SCRIPT_DIR/init_kong.sh" -n "$ns" stop || true
if [[ "$ENABLE_KERBEROS" == "1" ]]; then if [[ "$ENABLE_KERBEROS" == "1" && "${PROLE_KDC_STANDALONE:-0}" == "1" ]]; then
SERVICE_NAMESPACE="$ns" PROLE_KDC_NAMESPACE="$ns" \ SERVICE_NAMESPACE="$ns" PROLE_KDC_NAMESPACE="$ns" \
"$SCRIPT_DIR/init_kdc.sh" cleanup || true "$SCRIPT_DIR/init_kdc.sh" cleanup || true
fi fi

View File

@ -721,7 +721,7 @@ root = sys.argv[1]
pw = sys.argv[2] pw = sys.argv[2]
sys.path.insert(0, root) sys.path.insert(0, root)
from installer import config as inst_config from knoe import config as inst_config
print(inst_config._encrypt_prole_secret(pw)) print(inst_config._encrypt_prole_secret(pw))
PY PY

View File

@ -13,6 +13,11 @@ _prole_cfg_script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
_prole_cfg_home_guess=$(cd "$_prole_cfg_script_dir/.." && pwd) _prole_cfg_home_guess=$(cd "$_prole_cfg_script_dir/.." && pwd)
# 1. Try to load env.sh to establish base PROLE_HOME/PROLE_CONF # 1. Try to load env.sh to establish base PROLE_HOME/PROLE_CONF
# If PROLE_HOME/PROLE_CONF/etc are already set by the caller (e.g. tests),
# do not allow env.sh to clobber them.
_prole_cfg_preserve_home="${PROLE_HOME:-}"
_prole_cfg_preserve_conf="${PROLE_CONF:-}"
_prole_cfg_preserve_service="${PROLE_SERVICE:-}"
if [[ -n "${PROLE_HOME:-}" && -f "$PROLE_HOME/env.sh" ]]; then if [[ -n "${PROLE_HOME:-}" && -f "$PROLE_HOME/env.sh" ]]; then
# shellcheck disable=SC1090 # shellcheck disable=SC1090
source "$PROLE_HOME/env.sh" source "$PROLE_HOME/env.sh"
@ -24,6 +29,20 @@ elif [[ -f "$_prole_cfg_home_guess/env.sh" ]]; then
source "$_prole_cfg_home_guess/env.sh" source "$_prole_cfg_home_guess/env.sh"
fi fi
if [[ -n "$_prole_cfg_preserve_home" ]]; then
PROLE_HOME="$_prole_cfg_preserve_home"
export PROLE_HOME
fi
if [[ -n "$_prole_cfg_preserve_conf" ]]; then
PROLE_CONF="$_prole_cfg_preserve_conf"
export PROLE_CONF
fi
if [[ -n "$_prole_cfg_preserve_service" ]]; then
PROLE_SERVICE="$_prole_cfg_preserve_service"
export PROLE_SERVICE
fi
unset _prole_cfg_preserve_home _prole_cfg_preserve_conf _prole_cfg_preserve_service
_prole_trim() { _prole_trim() {
local s="$1" local s="$1"
s="${s#"${s%%[![:space:]]*}"}" s="${s#"${s%%[![:space:]]*}"}"
@ -441,6 +460,17 @@ if [[ -n "$_prole_cfg_file" ]]; then
if [[ -n "$_cfg_sns" ]]; then if [[ -n "$_cfg_sns" ]]; then
export SERVICE_NAMESPACE="$_cfg_sns" export SERVICE_NAMESPACE="$_cfg_sns"
fi fi
# Always prefer deployment/mode hint from prole.cfg (single source of truth).
# This prevents leaked environment values (e.g. PROLE_MODE=k3d) from forcing the wrong mode.
_cfg_dm=$(_prole_cfg_extract_key_in_files "DEPLOYMENT_MODE" "${_prole_cfg_files[@]}")
if [[ -z "$_cfg_dm" ]]; then
_cfg_dm=$(_prole_cfg_extract_key_in_files "prole.mode" "${_prole_cfg_files[@]}")
fi
if [[ -n "$_cfg_dm" ]]; then
DEPLOYMENT_MODE="$_cfg_dm"
export DEPLOYMENT_MODE
fi
_cfg_sh=$(_prole_cfg_extract_key_in_files "SERVICE_HOSTNAME" "${_prole_cfg_files[@]}") _cfg_sh=$(_prole_cfg_extract_key_in_files "SERVICE_HOSTNAME" "${_prole_cfg_files[@]}")
if [[ -z "$_cfg_sh" ]]; then if [[ -z "$_cfg_sh" ]]; then
_cfg_sh=$(_prole_cfg_extract_key_in_files "service_hostname" "${_prole_cfg_files[@]}") _cfg_sh=$(_prole_cfg_extract_key_in_files "service_hostname" "${_prole_cfg_files[@]}")
@ -462,7 +492,7 @@ if [[ -n "$_prole_cfg_file" ]]; then
if [[ -n "$_cfg_ctx" ]]; then if [[ -n "$_cfg_ctx" ]]; then
export KUBECONTEXT="$_cfg_ctx" export KUBECONTEXT="$_cfg_ctx"
fi fi
unset _cfg_ns _cfg_sns _cfg_sh _cfg_sbh _cfg_ctx _prole_cfg_files _prole_cfg_f unset _cfg_ns _cfg_sns _cfg_dm _cfg_sh _cfg_sbh _cfg_ctx _prole_cfg_files _prole_cfg_f
fi fi
if [[ -z "${PROLE_HOME:-}" && -d "$_prole_cfg_home_guess" ]]; then if [[ -z "${PROLE_HOME:-}" && -d "$_prole_cfg_home_guess" ]]; then

View File

@ -6,8 +6,8 @@ set -euo pipefail
# Purpose: # Purpose:
# - Thin wrapper around the Python-native cluster repair logic. # - Thin wrapper around the Python-native cluster repair logic.
# #
# The real orchestration now lives in `installer/core/actions.py` and is # The real orchestration now lives in `knoe/core/actions.py` and is
# invoked through `installer/core/repair_pipeline_cli.py`. # invoked through `knoe/core/repair_pipeline_cli.py`.
SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
ROOT_DIR=$(cd "$SCRIPT_DIR/.." && pwd) ROOT_DIR=$(cd "$SCRIPT_DIR/.." && pwd)
@ -88,4 +88,4 @@ if [[ -n "$MODE" ]]; then
fi fi
args+=("--config" "$PROLE_CONF") args+=("--config" "$PROLE_CONF")
exec python3 -m installer.core.repair_pipeline_cli "${args[@]}" exec python3 -m knoe.core.repair_pipeline_cli "${args[@]}"

View File

@ -6,7 +6,7 @@ from pathlib import Path
ROOT_DIR = Path(__file__).resolve().parents[1] ROOT_DIR = Path(__file__).resolve().parents[1]
sys.path.append(str(ROOT_DIR)) sys.path.append(str(ROOT_DIR))
from installer import config as inst_config from knoe import config as inst_config
def main(): def main():

View File

@ -12,7 +12,7 @@ import tkinter as tk # noqa: F401
from tkinter import ttk, scrolledtext, messagebox, filedialog # noqa: F401 from tkinter import ttk, scrolledtext, messagebox, filedialog # noqa: F401
from pathlib import Path # noqa: F401 from pathlib import Path # noqa: F401
from installer.ui.screens import ( from knoe.ui.screens import (
ProleInstaller, ProleInstaller,
ProleController, ProleController,
get_resource_path, get_resource_path,

7
k3s/registries.yaml Normal file
View File

@ -0,0 +1,7 @@
mirrors:
"k3d-prole-registry:5000":
endpoint:
- "http://k3d-prole-registry:5000"
"k3d-prole-data-registry:5000":
endpoint:
- "http://k3d-prole-data-registry:5000"

View File

@ -5,7 +5,7 @@ metadata:
name: prole-db name: prole-db
spec: spec:
instances: 3 instances: 3
imageName: myrddin.prole.org:5000/prole-db:0.0.0 imageName: k3d-prole-registry:5000/knoe-db:17-137
postgresUID: 100 postgresUID: 100
postgresGID: 101 postgresGID: 101
maxSyncReplicas: 1 maxSyncReplicas: 1

View File

@ -4,7 +4,7 @@ metadata:
name: prole-db name: prole-db
spec: spec:
instances: 3 instances: 3
imageName: myrddin.prole.org:5000/prole-db:18-125 imageName: myrddin.prole.org:5000/knoe-db:18-125
postgresUID: 100 postgresUID: 100
postgresGID: 101 postgresGID: 101
maxSyncReplicas: 1 maxSyncReplicas: 1

96
knoe.spec Normal file
View File

@ -0,0 +1,96 @@
# -*- mode: python ; coding: utf-8 -*-
import sys
from pathlib import Path
block_cipher = None
# Get project root
project_root = Path('.').absolute()
# Data files to include
datas = [
('installer', 'installer'),
('conf', 'conf'),
('etc', 'etc'),
('img', 'img'),
('docs', 'docs'),
('k8s', 'k8s'),
('prole-db', 'prole-db'),
('prole-app/dist/Prole Tools.app', 'prole-app/dist/Prole Tools.app'),
]
# Binaries to include (with execute permissions)
binaries = [
('prole-net/prole-agent', 'prole-net'),
]
# Hidden imports
hiddenimports = [
'installer',
'knoe.config',
'knoe.build',
'knoe.deploy',
'knoe.screen',
'knoe.main',
'knoe.ncurses_ui',
'knoe.ncurses_installer',
'curses',
'_curses',
]
a = Analysis(
['install.py'],
pathex=[],
binaries=binaries,
datas=datas,
hiddenimports=hiddenimports,
hookspath=[],
hooksconfig={},
runtime_hooks=[],
excludes=[],
win_no_prefer_redirects=False,
win_private_assemblies=False,
cipher=block_cipher,
noarchive=False,
)
pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher)
exe = EXE(
pyz,
a.scripts,
a.binaries,
a.zipfiles,
a.datas,
[],
name='prole-installer',
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=True,
upx_exclude=[],
runtime_tmpdir=None,
console=True, # Enable console for --no-gui mode
disable_windowed_traceback=False,
target_arch=None,
codesign_identity=None,
entitlements_file=None,
icon='build/prole.icns',
)
# Create macOS app bundle
app = BUNDLE(
exe,
name='Prole Installer.app',
icon='build/prole.icns',
bundle_identifier='com.prole.installer',
info_plist={
'CFBundleName': 'Prole Installer',
'CFBundleDisplayName': 'Prole Database Installer',
'CFBundleVersion': '1.0.0',
'CFBundleShortVersionString': '1.0.0',
'NSHighResolutionCapable': 'True',
'LSMinimumSystemVersion': '10.13.0',
},
)

View File

@ -61,7 +61,7 @@ class BuildMilestone(Milestone):
# ---------------- Screen (UI) helpers ---------------- # ---------------- Screen (UI) helpers ----------------
def create_build_page(app): def create_build_page(app):
"""Create the Build page UI and register it via installer.main.""" """Create the Build page UI and register it via knoe.main."""
import tkinter as tk import tkinter as tk
from tkinter import ttk, scrolledtext from tkinter import ttk, scrolledtext

View File

@ -1,8 +1,8 @@
""" """
Shared configuration and utility functions for the Prole installer (root-level). Shared configuration and utility functions for the Prole installer (root-level).
This mirrors `prole.installer.config` but is located under the root `installer/` This mirrors `prole.knoe.config` but is located under the root `installer/`
package per the refactor request. UI code should import from `installer.config`. package per the refactor request. UI code should import from `knoe.config`.
""" """
from __future__ import annotations from __future__ import annotations
@ -30,7 +30,7 @@ OPENBAO_PREFIX = "${OPENBAO:"
OPENBAO_SUFFIX = "}" OPENBAO_SUFFIX = "}"
PROLE_SECRET_VERSION = "v1" PROLE_SECRET_VERSION = "v1"
PROLE_SECRET_SERVICE = "prole-installer" PROLE_SECRET_SERVICE = "prole-installer"
PROLE_SECRET_KEY_FILE = Path.home() / ".prole" / "secrets" / "installer.key" PROLE_SECRET_KEY_FILE = Path.home() / ".prole" / "secrets" / "knoe.key"
# Map config keys to OpenBao paths (namespace injected at runtime) # Map config keys to OpenBao paths (namespace injected at runtime)
SECRET_KEY_SPECS = { SECRET_KEY_SPECS = {
@ -452,6 +452,83 @@ def _collect_cfg_vars(cfg: any) -> dict:
return variables return variables
def _collect_cfg_vars_from_data(cfg_data: dict | None) -> dict[str, str]:
"""Collect variable names from a `prole_cfg_data`-style dict.
This mirrors `_collect_cfg_vars()` but operates on the UI/controller's in-memory
dict instead of a `configparser.ConfigParser`.
"""
variables: dict[str, str] = {}
if not cfg_data or not isinstance(cfg_data, dict):
return variables
def _is_placeholder(v: str) -> bool:
vv = (v or "").strip()
return vv.startswith("${") and vv.endswith("}")
for section in ("Global", "System Environment", "User"):
sec = cfg_data.get(section, {}) or {}
if not isinstance(sec, dict):
continue
for k, v in sec.items():
s = str(v or "").strip()
if not s or _is_placeholder(s):
continue
variables[str(k)] = s
return variables
def _expand_cfg_vars_shellstyle(val: str, variables: dict[str, str]) -> str:
"""Expand `$VAR` and `${VAR}` using `variables`.
Notes:
- Only expands simple shell-style identifiers (letters/digits/underscore).
This intentionally avoids treating `${PROLE_SECRET:...}` or similar
colon-delimited references as variables.
- Unknown variables are left intact.
"""
if not val or not isinstance(val, str) or not variables:
return val
# ${VAR}
def repl_braced(m: re.Match) -> str:
name = m.group(1)
return variables.get(name, m.group(0))
out = re.sub(r"\$\{([A-Za-z_][A-Za-z0-9_]*)\}", repl_braced, val)
# $VAR (avoid $ followed by { ... } which is already handled above)
def repl_plain(m: re.Match) -> str:
name = m.group(1)
return variables.get(name, m.group(0))
out = re.sub(r"\$([A-Za-z_][A-Za-z0-9_]*)", repl_plain, out)
return out
def _expand_path_expr(val: str | None, variables: dict[str, str] | None = None) -> str:
"""Expand a path-like expression with support for config variables.
Expansion order (fixed-point loop):
1) `~` expansion
2) config variables (`$VAR` and `${VAR}`)
3) OS environment variables (via `os.path.expandvars`)
"""
if not val:
return ""
out = os.path.expanduser(str(val))
for _ in range(10):
new = out
if variables:
new = _expand_cfg_vars_shellstyle(new, variables)
new = os.path.expandvars(new)
if new == out:
break
out = new
return out
def _expand_cfg_value(val: str, variables: dict) -> str: def _expand_cfg_value(val: str, variables: dict) -> str:
if not val or not isinstance(val, str): if not val or not isinstance(val, str):
return val return val
@ -583,7 +660,7 @@ def _update_prole_cfg_value(section: str, key: str, value: str):
# Prefer `$PROLE_CONF/prole.cfg` (single entrypoint) and follow symlink so we # Prefer `$PROLE_CONF/prole.cfg` (single entrypoint) and follow symlink so we
# update the active environment base file without mutating other environments. # update the active environment base file without mutating other environments.
try: try:
from installer import prole_conf from knoe import prole_conf
conf_dir = prole_conf.resolve_prole_conf_dir(PROJECT_ROOT) conf_dir = prole_conf.resolve_prole_conf_dir(PROJECT_ROOT)
cfg_path = prole_conf.entrypoint_path(conf_dir) cfg_path = prole_conf.entrypoint_path(conf_dir)

View File

@ -15,7 +15,7 @@ import threading
import uuid import uuid
from typing import Callable, Sequence from typing import Callable, Sequence
from installer.config import ( from knoe.config import (
_expand_path, _expand_path,
_collect_cfg_vars, _collect_cfg_vars,
_expand_cfg_value, _expand_cfg_value,
@ -28,10 +28,10 @@ from installer.config import (
_encrypt_cfg_secret, _encrypt_cfg_secret,
_merge_kubeconfig, _merge_kubeconfig,
) )
from installer import prole_conf as prole_conf_mgr from knoe import prole_conf as prole_conf_mgr
from installer.core.controller import ProleController from knoe.core.controller import ProleController
from installer.core.env import * # noqa: F401,F403 from knoe.core.env import * # noqa: F401,F403
from installer.core.env import ( from knoe.core.env import (
_parse_ollama_host, _parse_ollama_host,
_bool_str, _bool_str,
_deployment_mode_from_env, _deployment_mode_from_env,
@ -63,7 +63,7 @@ from installer.core.env import (
_kubectl_base_cmd_for_k3s as _kubectl_base_cmd_for_k3s_fn, _kubectl_base_cmd_for_k3s as _kubectl_base_cmd_for_k3s_fn,
_verify_k3s_services_status, _verify_k3s_services_status,
) )
from installer.core.milestones import ( from knoe.core.milestones import (
DependenciesMilestone, DependenciesMilestone,
NetworkScanMilestone, NetworkScanMilestone,
EnvSetupMilestone, EnvSetupMilestone,
@ -78,8 +78,9 @@ from installer.core.milestones import (
SupabaseImagePreloadMilestone, SupabaseImagePreloadMilestone,
SupabaseMilestone, SupabaseMilestone,
) )
from installer.core.stream_exec import run_streaming_cmd from knoe.core.stream_exec import run_streaming_cmd
from installer.core.policy import ( from knoe.core.build_context import copy_build_context_dir
from knoe.core.policy import (
POLICY_CFG_KEY, POLICY_CFG_KEY,
OPTIONAL_WORKLOADS_MIN_READY_SCHEDULABLE_NODES, OPTIONAL_WORKLOADS_MIN_READY_SCHEDULABLE_NODES,
evaluate_optional_workloads_allowed, evaluate_optional_workloads_allowed,
@ -773,13 +774,13 @@ class ProleInstaller:
try: try:
return os.getlogin() return os.getlogin()
except Exception: except Exception:
return "prole-db" return "knoe"
def _sanitize_namespace(self, name: str) -> str: def _sanitize_namespace(self, name: str) -> str:
cleaned = re.sub(r"[^a-z0-9-]+", "-", (name or "").lower()) cleaned = re.sub(r"[^a-z0-9-]+", "-", (name or "").lower())
cleaned = re.sub(r"-{2,}", "-", cleaned).strip("-") cleaned = re.sub(r"-{2,}", "-", cleaned).strip("-")
if not cleaned: if not cleaned:
cleaned = "prole-db" cleaned = "knoe-db"
if len(cleaned) > 63: if len(cleaned) > 63:
cleaned = cleaned[:63].rstrip("-") cleaned = cleaned[:63].rstrip("-")
return cleaned return cleaned
@ -787,13 +788,13 @@ class ProleInstaller:
def _generate_namespace_name(self) -> str: def _generate_namespace_name(self) -> str:
owner = self._sanitize_namespace(self._get_local_owner()) owner = self._sanitize_namespace(self._get_local_owner())
suffix = uuid.uuid4().hex[:6] suffix = uuid.uuid4().hex[:6]
base = f"prole-db-{owner}-{suffix}" base = f"knoe-db-{owner}-{suffix}"
return self._sanitize_namespace(base) return self._sanitize_namespace(base)
def _ensure_namespace_prefix(self, name: str) -> str: def _ensure_namespace_prefix(self, name: str) -> str:
cleaned = (name or "").strip() cleaned = (name or "").strip()
if not cleaned: if not cleaned:
return "prole-db" return "knoe-db"
return cleaned return cleaned
def _read_existing_cfg_namespace(self) -> str | None: def _read_existing_cfg_namespace(self) -> str | None:
@ -1073,12 +1074,29 @@ class ProleInstaller:
cluster_env = self._get_input("init_cluster.cluster_env", "dev") cluster_env = self._get_input("init_cluster.cluster_env", "dev")
env_key = _normalize_cluster_env(cluster_env) env_key = _normalize_cluster_env(cluster_env)
home = Path(values["PROLE_HOME"]).expanduser() env_map = dict(os.environ)
try:
env_map.update({str(k): str(v) for k, v in (values or {}).items() if v is not None})
except Exception:
pass
expanded_home = self._expand_shell_path(values.get("PROLE_HOME", ""), env=env_map)
expanded_home = os.path.expanduser(expanded_home) if expanded_home else ""
if expanded_home:
env_map["PROLE_HOME"] = expanded_home
home = Path(expanded_home)
else:
home = Path.home() / ".prole"
if env_key == "dev": if env_key == "dev":
home.mkdir(parents=True, exist_ok=True) home.mkdir(parents=True, exist_ok=True)
for key in ("PROLE_CONF", "PROLE_DATA", "PROLE_LOGS", "PROLE_SERVICE"): for key in ("PROLE_CONF", "PROLE_DATA", "PROLE_LOGS", "PROLE_SERVICE"):
try: try:
Path(values[key]).expanduser().mkdir(parents=True, exist_ok=True) raw = values.get(key, "")
expanded = self._expand_shell_path(raw, env=env_map)
expanded = os.path.expanduser(expanded) if expanded else ""
if expanded:
env_map[key] = expanded
Path(expanded).mkdir(parents=True, exist_ok=True)
except Exception: except Exception:
pass pass
@ -1149,8 +1167,21 @@ class ProleInstaller:
def _deploy_env_resources(self, values: dict): def _deploy_env_resources(self, values: dict):
"""Copy init scripts and etc directory into PROLE_HOME / PROLE_SERVICE.""" """Copy init scripts and etc directory into PROLE_HOME / PROLE_SERVICE."""
try: try:
prole_home = Path(values["PROLE_HOME"]).expanduser() env_map = dict(os.environ)
prole_service = Path(values["PROLE_SERVICE"]).expanduser() try:
env_map.update({str(k): str(v) for k, v in (values or {}).items() if v is not None})
except Exception:
pass
expanded_home = self._expand_shell_path(values.get("PROLE_HOME", ""), env=env_map)
expanded_home = os.path.expanduser(expanded_home) if expanded_home else ""
if expanded_home:
env_map["PROLE_HOME"] = expanded_home
expanded_service = self._expand_shell_path(values.get("PROLE_SERVICE", ""), env=env_map)
expanded_service = os.path.expanduser(expanded_service) if expanded_service else ""
prole_home = Path(expanded_home or (Path.home() / ".prole"))
prole_service = Path(expanded_service or (prole_home / "etc"))
init_pf_src_candidates = [ init_pf_src_candidates = [
self.project_root / "src" / "prole" / "etc" / "init-port-forward.sh", self.project_root / "src" / "prole" / "etc" / "init-port-forward.sh",
@ -1184,9 +1215,10 @@ class ProleInstaller:
pass pass
def reload_env_from_shell(self) -> None: def reload_env_from_shell(self) -> None:
home = Path( raw_home = self._get_input("env_setup.PROLE_HOME", str(Path.home() / ".prole"))
self._get_input("env_setup.PROLE_HOME", str(Path.home() / ".prole")) expanded_home = self._expand_shell_path(raw_home, env=os.environ)
) expanded_home = os.path.expanduser(expanded_home) if expanded_home else ""
home = Path(expanded_home or str(Path.home() / ".prole"))
env_file = home / "env.sh" env_file = home / "env.sh"
cmd = ( cmd = (
f"export PROLE_HOME={shlex.quote(str(home))}; " f"export PROLE_HOME={shlex.quote(str(home))}; "
@ -4271,7 +4303,7 @@ class ProleConsoleInstaller(ProleInstaller):
return return
tag = self.controller.get_prole_db_version() tag = self.controller.get_prole_db_version()
image_name = f"prole-db:{tag}" image_name = f"knoe-db:{tag}"
env_key = _normalize_cluster_env( env_key = _normalize_cluster_env(
self._get_input("init_cluster.cluster_env", "dev") self._get_input("init_cluster.cluster_env", "dev")
) )
@ -4309,7 +4341,7 @@ class ProleConsoleInstaller(ProleInstaller):
if not registry or registry == "localhost:5000": if not registry or registry == "localhost:5000":
push_ok = False push_ok = False
self.err( self.err(
"[ERROR] k3s mode but registry host could not be resolved; cannot push prole-db image." "[ERROR] k3s mode but registry host could not be resolved; cannot push knoe-db image."
) )
else: else:
remote_tag = f"{registry}/{image_name}" remote_tag = f"{registry}/{image_name}"
@ -4337,16 +4369,12 @@ class ProleConsoleInstaller(ProleInstaller):
) )
return return
self.log("==> Build prole-db image") self.log("==> Build knoe-db image")
prole_home = Path.home() / ".prole" prole_home = Path.home() / ".prole"
build_dir = prole_home / "build" / "prole-db" build_dir = prole_home / "build" / "prole-db"
build_dir.mkdir(parents=True, exist_ok=True) build_dir.mkdir(parents=True, exist_ok=True)
source_dir = get_resource_path("prole-db") source_dir = get_resource_path("prole-db")
if source_dir.exists(): copy_build_context_dir(source_dir, build_dir)
if source_dir.resolve() != build_dir.resolve():
if build_dir.exists():
shutil.rmtree(build_dir)
shutil.copytree(source_dir, build_dir)
pub_key_path = Path.home() / ".ssh" / "id_prole_ed25519.pub" pub_key_path = Path.home() / ".ssh" / "id_prole_ed25519.pub"
pub_key = pub_key_path.read_text().strip() if pub_key_path.exists() else "" pub_key = pub_key_path.read_text().strip() if pub_key_path.exists() else ""
@ -5117,7 +5145,14 @@ class ProleConsoleInstaller(ProleInstaller):
env_key = _normalize_cluster_env( env_key = _normalize_cluster_env(
self._get_input("init_cluster.cluster_env", "") self._get_input("init_cluster.cluster_env", "")
) )
if env_key != "service": mode = _deployment_mode_from_env(self._get_input("init_cluster.mode", ""))
if not mode:
mode = _deployment_mode_from_env(os.environ.get("PROLE_MODE", ""))
# OpenTofu pipeline is k3s-scoped, but we still want it available for k3d
# installs when the user provides k3s connection settings (e.g., staging
# manifests for a service cluster while running a local dev cluster).
if env_key != "service" and mode not in ("k3d", "k3s"):
return return
namespace = ( namespace = (
self._get_input("init_password.db_namespace", "") or "" self._get_input("init_password.db_namespace", "") or ""
@ -5611,63 +5646,63 @@ def _prepare_k3s_pipeline(
_log("==> Loading installer config...\n") _log("==> Loading installer config...\n")
installer = ProleConsoleInstaller(controller, str(cfg_path)) installer = ProleConsoleInstaller(controller, str(cfg_path))
try: try:
existing_inputs = installer._load_inputs_from_cfg() existing_inputs = knoe._load_inputs_from_cfg()
except Exception: except Exception:
existing_inputs = {} existing_inputs = {}
installer.inputs = {**installer._default_inputs(), **existing_inputs} knoe.inputs = {**knoe._default_inputs(), **existing_inputs}
if k3s_server and not installer.inputs.get("init_cluster.k3s_server_url"): if k3s_server and not knoe.inputs.get("init_cluster.k3s_server_url"):
installer.inputs["init_cluster.k3s_server_url"] = k3s_server knoe.inputs["init_cluster.k3s_server_url"] = k3s_server
if k3s_token and not installer.inputs.get("init_cluster.k3s_token"): if k3s_token and not knoe.inputs.get("init_cluster.k3s_token"):
installer.inputs["init_cluster.k3s_token"] = k3s_token knoe.inputs["init_cluster.k3s_token"] = k3s_token
namespace = (installer._get_input("init_password.db_namespace", "") or "").strip() namespace = (knoe._get_input("init_password.db_namespace", "") or "").strip()
if not namespace: if not namespace:
namespace = ( namespace = (
installer._get_input("env_setup.NAMESPACE", "") or "" knoe._get_input("env_setup.NAMESPACE", "") or ""
).strip() or "default" ).strip() or "default"
_log(f" Target namespace: {namespace}\n") _log(f" Target namespace: {namespace}\n")
_log(" [OK] Config loaded\n\n") _log(" [OK] Config loaded\n\n")
if not skip_validation: if not skip_validation:
# Standalone mode: write a pipeline-specific cfg and validate # Standalone mode: write a pipeline-specific cfg and validate
installer.inputs["init_cluster.cluster_env"] = "prole-service-cluster" knoe.inputs["init_cluster.cluster_env"] = "prole-service-cluster"
installer.inputs["init_cluster.supabase_enabled"] = _bool_str(False) knoe.inputs["init_cluster.supabase_enabled"] = _bool_str(False)
installer.inputs["init_cluster.kerberos_enabled"] = _bool_str(False) knoe.inputs["init_cluster.kerberos_enabled"] = _bool_str(False)
installer.inputs["init_cluster.at_rest_encryption_enabled"] = _bool_str(True) knoe.inputs["init_cluster.at_rest_encryption_enabled"] = _bool_str(True)
installer.inputs["kerberos_config.enabled"] = _bool_str(False) knoe.inputs["kerberos_config.enabled"] = _bool_str(False)
installer.inputs["kerberos_config.test_connection"] = _bool_str(False) knoe.inputs["kerberos_config.test_connection"] = _bool_str(False)
installer.inputs["env_setup.PROLE_HOME"] = str(project_root) knoe.inputs["env_setup.PROLE_HOME"] = str(project_root)
installer.inputs["env_setup.PROLE_CONF"] = str(project_root / "conf") knoe.inputs["env_setup.PROLE_CONF"] = str(project_root / "conf")
installer.inputs["env_setup.PROLE_DATA"] = installer._resolve_env_value( knoe.inputs["env_setup.PROLE_DATA"] = knoe._resolve_env_value(
"PROLE_DATA", str(Path.home() / ".prole" / "data") "PROLE_DATA", str(Path.home() / ".prole" / "data")
) or str(Path.home() / ".prole" / "data") ) or str(Path.home() / ".prole" / "data")
installer.inputs["env_setup.PROLE_LOGS"] = installer._resolve_env_value( knoe.inputs["env_setup.PROLE_LOGS"] = knoe._resolve_env_value(
"PROLE_LOGS", str(Path.home() / ".prole" / "logs") "PROLE_LOGS", str(Path.home() / ".prole" / "logs")
) or str(Path.home() / ".prole" / "logs") ) or str(Path.home() / ".prole" / "logs")
installer.inputs["env_setup.PROLE_SERVICE"] = str(project_root / "etc") knoe.inputs["env_setup.PROLE_SERVICE"] = str(project_root / "etc")
# Ensure required keys that the silent install test validates # Ensure required keys that the silent install test validates
if not installer.inputs.get("env_setup.NAMESPACE"): if not knoe.inputs.get("env_setup.NAMESPACE"):
installer.inputs["env_setup.NAMESPACE"] = ( knoe.inputs["env_setup.NAMESPACE"] = (
installer.inputs.get("init_password.db_namespace", "") or "default" knoe.inputs.get("init_password.db_namespace", "") or "default"
) )
if not installer.inputs.get("init_password.db_namespace"): if not knoe.inputs.get("init_password.db_namespace"):
installer.inputs["init_password.db_namespace"] = ( knoe.inputs["init_password.db_namespace"] = (
installer.inputs.get("env_setup.NAMESPACE", "") or "default" knoe.inputs.get("env_setup.NAMESPACE", "") or "default"
) )
if not installer.inputs.get("kerberos_config.init_authority"): if not knoe.inputs.get("kerberos_config.init_authority"):
installer.inputs["kerberos_config.init_authority"] = _bool_str(False) knoe.inputs["kerberos_config.init_authority"] = _bool_str(False)
if not installer.inputs.get("init_password.db_host_port"): if not knoe.inputs.get("init_password.db_host_port"):
installer.inputs["init_password.db_host_port"] = "5432" knoe.inputs["init_password.db_host_port"] = "5432"
_log(" [OK] Config overrides applied for k3s pipeline\n") _log(" [OK] Config overrides applied for k3s pipeline\n")
_log("==> Writing prole.cfg...\n") _log("==> Writing prole.cfg...\n")
installer._write_cfg() knoe._write_cfg()
_log(f" [OK] {cfg_path}\n\n") _log(f" [OK] {cfg_path}\n\n")
db_password = installer._get_input("init_password.db_password", "").strip() db_password = knoe._get_input("init_password.db_password", "").strip()
_log(f"==> Target namespace: {namespace}\n\n") _log(f"==> Target namespace: {namespace}\n\n")
attempt = 1 attempt = 1
@ -5685,8 +5720,8 @@ def _prepare_k3s_pipeline(
break break
_log(f" [WARN] Silent install test failed (code {rc})\n") _log(f" [WARN] Silent install test failed (code {rc})\n")
cur_server = installer.inputs.get("init_cluster.k3s_server_url", k3s_server) cur_server = knoe.inputs.get("init_cluster.k3s_server_url", k3s_server)
cur_token = installer.inputs.get("init_cluster.k3s_token", k3s_token) cur_token = knoe.inputs.get("init_cluster.k3s_token", k3s_token)
if attempt == 1: if attempt == 1:
_log(" Attempting k3s repair...\n") _log(" Attempting k3s repair...\n")
@ -5706,8 +5741,8 @@ def _prepare_k3s_pipeline(
else: else:
_log("==> Skipping validation (silent install already succeeded)\n\n") _log("==> Skipping validation (silent install already succeeded)\n\n")
cur_server = installer.inputs.get("init_cluster.k3s_server_url", k3s_server) cur_server = knoe.inputs.get("init_cluster.k3s_server_url", k3s_server)
cur_token = installer.inputs.get("init_cluster.k3s_token", k3s_token) cur_token = knoe.inputs.get("init_cluster.k3s_token", k3s_token)
_sync_opentofu_pipeline(project_root, namespace, cur_server, cur_token, log_fn=_log) _sync_opentofu_pipeline(project_root, namespace, cur_server, cur_token, log_fn=_log)
_log("\n==> k3s pipeline preparation complete.\n") _log("\n==> k3s pipeline preparation complete.\n")

View File

@ -0,0 +1,66 @@
"""Helpers for preparing Docker build contexts.
These functions exist to make Docker builds robust when running from:
- a source checkout (may contain runtime artifacts like `prole-db/data/`)
- a packaged distribution (resources extracted to a temp dir)
The Docker build context should only contain source-controlled inputs needed
for the image build. Runtime data directories can contain broken symlinks,
concurrently-mutating files, or large volumes that should never be copied.
"""
from __future__ import annotations
import shutil
from pathlib import Path
from typing import Iterable
DEFAULT_IGNORED_NAMES: tuple[str, ...] = (
# Runtime/stateful artifacts (can be huge and may contain dangling paths)
"data",
# Common VCS/cache noise
".git",
"__pycache__",
".pytest_cache",
)
def copy_build_context_dir(
source_dir: Path,
build_dir: Path,
*,
ignored_names: Iterable[str] = DEFAULT_IGNORED_NAMES,
) -> None:
"""Copy a directory tree into `build_dir` for use as a Docker build context.
- Removes any existing `build_dir` first.
- Preserves symlinks (does not follow them).
- Ignores selected directory/file names anywhere in the tree.
"""
source_dir = Path(source_dir)
build_dir = Path(build_dir)
if source_dir.exists() is False:
return
if source_dir.resolve() == build_dir.resolve():
# Nothing to do; copying onto itself is both pointless and dangerous.
return
ignored_set = set(ignored_names)
def _ignore(_dir: str, names: list[str]) -> set[str]:
return {name for name in names if name in ignored_set}
if build_dir.exists():
shutil.rmtree(build_dir)
shutil.copytree(
source_dir,
build_dir,
symlinks=True,
ignore=_ignore,
ignore_dangling_symlinks=True,
)

View File

@ -7,10 +7,11 @@ import logging
from pathlib import Path from pathlib import Path
from typing import TYPE_CHECKING, Any, Callable, Sequence from typing import TYPE_CHECKING, Any, Callable, Sequence
from installer.state import InstallerState from knoe.state import InstallerState
from knoe.core.env import _expand_cfg_value
if TYPE_CHECKING: if TYPE_CHECKING:
from installer.milestone import Milestone from knoe.milestone import Milestone
class ProleController: class ProleController:
@ -61,15 +62,26 @@ class ProleController:
if args is None: if args is None:
args = [] args = []
base_env = os.environ.copy()
if env:
try:
base_env.update(env)
except Exception:
pass
# Get paths # Get paths
source_script = self.project_root / "etc" / script_name source_script = self.project_root / "etc" / script_name
# Determine target etc directory # Determine target etc directory
prole_home_val = (env or os.environ).get("PROLE_HOME") prole_home_val = base_env.get("PROLE_HOME")
if not prole_home_val: if not prole_home_val:
prole_home = Path.home() / ".prole" prole_home = Path.home() / ".prole"
else: else:
prole_home = Path(prole_home_val).expanduser() expanded_home = _expand_cfg_value(
str(prole_home_val), env=base_env, max_depth=10
)
expanded_home = os.path.expanduser(expanded_home)
prole_home = Path(expanded_home)
target_etc = prole_home / "etc" target_etc = prole_home / "etc"
target_etc.mkdir(parents=True, exist_ok=True) target_etc.mkdir(parents=True, exist_ok=True)
@ -178,7 +190,24 @@ class ProleController:
self.logger.debug(f"Starting subprocess: {' '.join(cmd)}") self.logger.debug(f"Starting subprocess: {' '.join(cmd)}")
# Ensure PYTHONUNBUFFERED=1 for any python scripts called within the bash script # Ensure PYTHONUNBUFFERED=1 for any python scripts called within the bash script
run_env = (env or os.environ).copy() run_env = base_env.copy()
# Expand PROLE_* paths so subprocesses never see literal `$HOME`/`$PROLE_HOME`.
try:
run_env["PROLE_HOME"] = str(prole_home)
for k in (
"PROLE_CONF",
"PROLE_DATA",
"PROLE_LOGS",
"PROLE_SERVICE",
"KUBECONFIG",
):
raw = (run_env.get(k) or "").strip()
if not raw or "$" not in raw:
continue
expanded = _expand_cfg_value(raw, env=run_env, max_depth=10)
run_env[k] = os.path.expanduser(expanded)
except Exception:
pass
self.logger.debug( self.logger.debug(
f" cwd={os.getcwd()} KUBECONFIG={run_env.get('KUBECONFIG','<unset>')} NAMESPACE={run_env.get('NAMESPACE','<unset>')} PROLE_MODE={run_env.get('PROLE_MODE','<unset>')}" f" cwd={os.getcwd()} KUBECONFIG={run_env.get('KUBECONFIG','<unset>')} NAMESPACE={run_env.get('NAMESPACE','<unset>')} PROLE_MODE={run_env.get('PROLE_MODE','<unset>')}"
) )

View File

@ -23,9 +23,9 @@ from pathlib import Path
from cryptography.hazmat.primitives.ciphers.aead import AESGCM from cryptography.hazmat.primitives.ciphers.aead import AESGCM
from installer import config as inst_config from knoe import config as inst_config
from installer import prole_conf from knoe import prole_conf
from installer.core.policy import OPTIONAL_WORKLOADS_MIN_READY_SCHEDULABLE_NODES, POLICY_CFG_KEY from knoe.core.policy import OPTIONAL_WORKLOADS_MIN_READY_SCHEDULABLE_NODES, POLICY_CFG_KEY
from typing import Any from typing import Any
@ -84,7 +84,7 @@ OPENBAO_PREFIX = "${OPENBAO:"
OPENBAO_SUFFIX = "}" OPENBAO_SUFFIX = "}"
PROLE_SECRET_VERSION = "v1" PROLE_SECRET_VERSION = "v1"
PROLE_SECRET_SERVICE = "prole-installer" PROLE_SECRET_SERVICE = "prole-installer"
PROLE_SECRET_KEY_FILE = Path.home() / ".prole" / "secrets" / "installer.key" PROLE_SECRET_KEY_FILE = Path.home() / ".prole" / "secrets" / "knoe.key"
# Map config keys to OpenBao paths (namespace injected at runtime) # Map config keys to OpenBao paths (namespace injected at runtime)
SECRET_KEY_SPECS = { SECRET_KEY_SPECS = {

View File

@ -7,19 +7,19 @@ import time
from pathlib import Path from pathlib import Path
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from installer.milestone import Milestone from knoe.milestone import Milestone
from installer import config as inst_config from knoe import config as inst_config
from installer import prole_conf from knoe import prole_conf
from installer.core.stream_exec import run_streaming_cmd from knoe.core.stream_exec import run_streaming_cmd
from installer.core.policy import ( from knoe.core.policy import (
POLICY_CFG_KEY, POLICY_CFG_KEY,
OPTIONAL_WORKLOADS_MIN_READY_SCHEDULABLE_NODES, OPTIONAL_WORKLOADS_MIN_READY_SCHEDULABLE_NODES,
evaluate_optional_workloads_allowed, evaluate_optional_workloads_allowed,
) )
if TYPE_CHECKING: if TYPE_CHECKING:
from installer.state import InstallerState from knoe.state import InstallerState
from installer.milestone import ProgressCallback from knoe.milestone import ProgressCallback
def _stream_line(line: str) -> None: def _stream_line(line: str) -> None:
@ -376,10 +376,10 @@ class DockerBuildMilestone(Milestone):
return return
# In k3s/service installs, `init_cloudnative_pg.sh` already has a robust # In k3s/service installs, `init_cloudnative_pg.sh` already has a robust
# pre-flight that ensures the `prole-db` image is present/pushed (and can # pre-flight that ensures the `knoe-db` image is present/pushed (and can
# build it if missing). Building a multi-platform image here is expensive # build it if missing). Building a multi-platform image here is expensive
# and redundant when initialization scripts are enabled. # and redundant when initialization scripts are enabled.
from installer.core.env import _normalize_cluster_env from knoe.core.env import _normalize_cluster_env
env_key = ( env_key = (
_normalize_cluster_env(state.inputs.get("init_cluster.cluster_env", "dev")) _normalize_cluster_env(state.inputs.get("init_cluster.cluster_env", "dev"))
@ -409,7 +409,7 @@ class DockerBuildMilestone(Milestone):
server = server.split("://")[1] server = server.split("://")[1]
if ":" in server: if ":" in server:
server = server.split(":")[0] server = server.split(":")[0]
args = ["--tag", f"{server}:5000/prole-db:{tag}", "--push"] args = ["--tag", f"{server}:5000/knoe-db:{tag}", "--push"]
rc = state.controller.run_script("build_db.sh", args=args, on_line=_stream_line) rc = state.controller.run_script("build_db.sh", args=args, on_line=_stream_line)
if rc != 0: if rc != 0:
@ -427,7 +427,7 @@ class ClusterLifecycleMilestone(Milestone):
def execute( def execute(
self, state: InstallerState, progress: ProgressCallback | None = None self, state: InstallerState, progress: ProgressCallback | None = None
) -> None: ) -> None:
from installer.core.env import _normalize_cluster_env from knoe.core.env import _normalize_cluster_env
cluster_env = state.inputs.get("init_cluster.cluster_env", "dev") cluster_env = state.inputs.get("init_cluster.cluster_env", "dev")
env_key = _normalize_cluster_env(cluster_env) or cluster_env env_key = _normalize_cluster_env(cluster_env) or cluster_env
@ -465,7 +465,7 @@ class ClusterLifecycleMilestone(Milestone):
) )
res_stdout = res.stdout or "" res_stdout = res.stdout or ""
if cluster_name not in res_stdout: if cluster_name not in res_stdout:
from installer.core.env import _k3d_prole_data_volume_args from knoe.core.env import _k3d_prole_data_volume_args
prole_data = state.inputs.get( prole_data = state.inputs.get(
"env_setup.PROLE_DATA", "env_setup.PROLE_DATA",
@ -495,9 +495,9 @@ class ClusterLifecycleMilestone(Milestone):
except Exception: except Exception:
pass pass
# Ensure prole-db image is available in the cluster (avoid registry pull issues). # Ensure knoe-db image is available in the cluster (avoid registry pull issues).
tag = state.controller.get_prole_db_version() tag = state.controller.get_prole_db_version()
local_image = f"prole-db:{tag}" local_image = f"knoe-db:{tag}"
if ( if (
subprocess.run( subprocess.run(
["docker", "image", "inspect", local_image], capture_output=True ["docker", "image", "inspect", local_image], capture_output=True
@ -512,7 +512,7 @@ class ClusterLifecycleMilestone(Milestone):
registry = registry.replace(".localhost", "") registry = registry.replace(".localhost", "")
if not registry: if not registry:
registry = "k3d-prole-registry:5000" registry = "k3d-prole-registry:5000"
remote_tag = f"{registry}/prole-db:{tag}" remote_tag = f"{registry}/knoe-db:{tag}"
subprocess.run( subprocess.run(
["docker", "tag", local_image, remote_tag], capture_output=True ["docker", "tag", local_image, remote_tag], capture_output=True
) )
@ -932,7 +932,7 @@ class KerberosMilestone(Milestone):
def _resolve_supabase_deploy_mode(state: InstallerState) -> str: def _resolve_supabase_deploy_mode(state: InstallerState) -> str:
from installer.core.env import _normalize_cluster_env from knoe.core.env import _normalize_cluster_env
deploy_mode = ( deploy_mode = (
os.environ.get("SUPABASE_DEPLOY_MODE") or os.environ.get("SUPABASE_MODE") or "" os.environ.get("SUPABASE_DEPLOY_MODE") or os.environ.get("SUPABASE_MODE") or ""
@ -980,7 +980,7 @@ class SupabaseImagePreloadMilestone(Milestone):
progress(f"Supabase image preload skipped by policy: {opt_reason}", 1.0) progress(f"Supabase image preload skipped by policy: {opt_reason}", 1.0)
return return
from installer.core.env import _parse_bool from knoe.core.env import _parse_bool
env = self._get_script_env(state) env = self._get_script_env(state)
script_path = state.controller.project_root / "supabase" / "deploy.sh" script_path = state.controller.project_root / "supabase" / "deploy.sh"
@ -1061,7 +1061,7 @@ class SupabaseMilestone(Milestone):
progress(f"Supabase skipped by policy: {opt_reason}", 1.0) progress(f"Supabase skipped by policy: {opt_reason}", 1.0)
return return
from installer.core.env import _parse_bool from knoe.core.env import _parse_bool
env = self._get_script_env(state) env = self._get_script_env(state)
script_path = state.controller.project_root / "supabase" / "deploy.sh" script_path = state.controller.project_root / "supabase" / "deploy.sh"

View File

@ -9,9 +9,9 @@ import sys
import time import time
from pathlib import Path from pathlib import Path
from installer.config import _expand_path, _collect_cfg_vars, _expand_cfg_value from knoe.config import _expand_path, _collect_cfg_vars, _expand_cfg_value
from installer import prole_conf from knoe import prole_conf
from installer.core.env import ( from knoe.core.env import (
_build_required_port_forwards, _build_required_port_forwards,
_pf_extract_id, _pf_extract_id,
_pf_mapping_str, _pf_mapping_str,

View File

@ -15,7 +15,7 @@ import threading
from pathlib import Path from pathlib import Path
from typing import Callable from typing import Callable
from installer.core.env import DEFAULT_OLLAMA_PORT from knoe.core.env import DEFAULT_OLLAMA_PORT
def _is_ip(name: str) -> bool: def _is_ip(name: str) -> bool:

View File

@ -5,9 +5,9 @@ import os
import sys import sys
from pathlib import Path from pathlib import Path
from installer import prole_conf from knoe import prole_conf
from installer.core.controller import ProleController from knoe.core.controller import ProleController
from installer.core.actions import ProleConsoleInstaller from knoe.core.actions import ProleConsoleInstaller
def _project_root() -> Path: def _project_root() -> Path:
@ -84,42 +84,42 @@ def main(argv: list[str] | None = None) -> int:
installer = ProleConsoleInstaller(controller, cfg_path=cfg) installer = ProleConsoleInstaller(controller, cfg_path=cfg)
try: try:
existing_inputs = installer._load_inputs_from_cfg() existing_inputs = knoe._load_inputs_from_cfg()
except Exception: except Exception:
existing_inputs = {} existing_inputs = {}
installer.inputs = {**installer._default_inputs(), **existing_inputs} knoe.inputs = {**knoe._default_inputs(), **existing_inputs}
# Ensure mode influences the env we build (KUBECONFIG resolution, etc.). # Ensure mode influences the env we build (KUBECONFIG resolution, etc.).
if mode == "k3s": if mode == "k3s":
installer.inputs["init_cluster.cluster_env"] = "service" knoe.inputs["init_cluster.cluster_env"] = "service"
elif mode == "k3d": elif mode == "k3d":
installer.inputs["init_cluster.cluster_env"] = "dev" knoe.inputs["init_cluster.cluster_env"] = "dev"
elif mode == "k8s": elif mode == "k8s":
installer.inputs["init_cluster.cluster_env"] = "prod" knoe.inputs["init_cluster.cluster_env"] = "prod"
service_ns = (args.service_namespace or "").strip() or installer._service_namespace() service_ns = (args.service_namespace or "").strip() or knoe._service_namespace()
db_ns = ( db_ns = (
(args.db_namespace or "").strip() (args.db_namespace or "").strip()
or (installer._get_input("init_password.db_namespace", "") or "").strip() or (knoe._get_input("init_password.db_namespace", "") or "").strip()
or service_ns or service_ns
or "default" or "default"
) )
if args.db_namespace: if args.db_namespace:
installer.inputs["init_password.db_namespace"] = db_ns knoe.inputs["init_password.db_namespace"] = db_ns
env = installer._script_env_for_namespace(service_ns) env = knoe._script_env_for_namespace(service_ns)
env["SERVICE_NAMESPACE"] = service_ns env["SERVICE_NAMESPACE"] = service_ns
env["DB_NAMESPACE"] = db_ns env["DB_NAMESPACE"] = db_ns
env["NAMESPACE"] = service_ns env["NAMESPACE"] = service_ns
anomalies: list[str] = [] anomalies: list[str] = []
if installer._dashboard_kong_missing(env): if knoe._dashboard_kong_missing(env):
anomalies.append("dashboard") anomalies.append("dashboard")
if installer._authority_context_missing(): if knoe._authority_context_missing():
anomalies.append("authority") anomalies.append("authority")
installer._repair_stale_released_pvs(env) knoe._repair_stale_released_pvs(env)
installer._run_cluster_repair_pipeline(env=env, anomalies=anomalies) knoe._run_cluster_repair_pipeline(env=env, anomalies=anomalies)
return 0 return 0

View File

@ -180,7 +180,7 @@ def _create_deploy_row(app, parent, step: dict):
def create_deploy_page(app): def create_deploy_page(app):
"""Create the Deploy page UI and register it via installer.main. """Create the Deploy page UI and register it via knoe.main.
Final milestone flow for Deploy page: Final milestone flow for Deploy page:
- Show completed steps from earlier phases - Show completed steps from earlier phases

View File

@ -29,12 +29,12 @@ def clear_canvas_page(app):
def create_page_build(app): def create_page_build(app):
# Delegates build page UI creation to installer.build # Delegates build page UI creation to knoe.build
return build_mod.create_build_page(app) return build_mod.create_build_page(app)
def create_page_deploy(app): def create_page_deploy(app):
# Delegates deploy page UI creation to installer.deploy # Delegates deploy page UI creation to knoe.deploy
return deploy_mod.create_deploy_page(app) return deploy_mod.create_deploy_page(app)

View File

@ -110,7 +110,7 @@ class Milestone(ABC):
service_ns = env["NAMESPACE"] service_ns = env["NAMESPACE"]
env["SERVICE_NAMESPACE"] = service_ns env["SERVICE_NAMESPACE"] = service_ns
from installer.core.env import _deployment_mode_from_env from knoe.core.env import _deployment_mode_from_env
cluster_env = state.inputs.get("init_cluster.cluster_env", "dev") cluster_env = state.inputs.get("init_cluster.cluster_env", "dev")
mode = _deployment_mode_from_env(cluster_env) or "k3d" mode = _deployment_mode_from_env(cluster_env) or "k3d"

View File

@ -970,7 +970,7 @@ def run_ncurses_installer(controller):
def _main(stdscr): def _main(stdscr):
installer = ProleNcursesInstaller(stdscr, controller) installer = ProleNcursesInstaller(stdscr, controller)
installer.run() knoe.run()
try: try:
curses.wrapper(_main) curses.wrapper(_main)

7
knoe/ops/__init__.py Normal file
View File

@ -0,0 +1,7 @@
from __future__ import annotations
from .context import KnoeContext
__all__ = [
"KnoeContext",
]

52
knoe/ops/cli.py Normal file
View File

@ -0,0 +1,52 @@
from __future__ import annotations
import argparse
import importlib
import os
import sys
from pathlib import Path
from .context import KnoeContext
def _project_root() -> Path:
# <root>/knoe/ops/cli.py -> parents: ops, knoe, root
return Path(__file__).resolve().parents[2]
def main(argv: list[str] | None = None) -> int:
p = argparse.ArgumentParser(prog="knoe-ops", add_help=True)
p.add_argument("component", help="component name (e.g. common_core, openbao)")
p.add_argument(
"action",
help="action to run (initialize, start, update, restart, stop, status)",
)
p.add_argument("--mode", default=os.environ.get("PROLE_MODE", "dev"))
p.add_argument("--namespace", default=os.environ.get("PROLE_NAMESPACE", "default"))
p.add_argument(
"--service-namespace",
default=os.environ.get("PROLE_SERVICE_NAMESPACE", "default"),
)
p.add_argument("--config", dest="config", default=os.environ.get("PROLE_CFG"))
args = p.parse_args(argv)
cfg_path = Path(args.config).expanduser().resolve() if args.config else None
ctx = KnoeContext(
project_root=_project_root(),
cfg_path=cfg_path,
mode=args.mode,
namespace=args.namespace,
service_namespace=args.service_namespace,
env=dict(os.environ),
logger=lambda m: print(m),
err_logger=lambda m: print(m, file=sys.stderr),
)
mod = importlib.import_module(f"knoe.ops.components.{args.component}")
fn = getattr(mod, args.action)
rc = fn(ctx)
return 0 if rc is None else int(rc)
if __name__ == "__main__": # pragma: no cover
raise SystemExit(main())

View File

@ -0,0 +1,10 @@
from __future__ import annotations
__all__ = [
"argocd",
"common_core",
"garage_store",
"openbao",
"opentofu",
"registry",
]

View File

@ -0,0 +1,31 @@
from __future__ import annotations
from ..context import KnoeContext
from ..legacy_shell import run_script
_SCRIPT = "init_argocd.sh"
def initialize(ctx: KnoeContext):
return run_script(ctx, _SCRIPT, "initialize")
def start(ctx: KnoeContext):
return run_script(ctx, _SCRIPT, "start")
def update(ctx: KnoeContext):
return run_script(ctx, _SCRIPT, "update")
def restart(ctx: KnoeContext):
return run_script(ctx, _SCRIPT, "restart")
def stop(ctx: KnoeContext):
return run_script(ctx, _SCRIPT, "stop")
def status(ctx: KnoeContext):
return run_script(ctx, _SCRIPT, "status")

View File

@ -0,0 +1,31 @@
from __future__ import annotations
from ..context import KnoeContext
from ..legacy_shell import run_script
_SCRIPT = "init_common_core.sh"
def initialize(ctx: KnoeContext):
return run_script(ctx, _SCRIPT, "initialize")
def start(ctx: KnoeContext):
return run_script(ctx, _SCRIPT, "start")
def update(ctx: KnoeContext):
return run_script(ctx, _SCRIPT, "update")
def restart(ctx: KnoeContext):
return run_script(ctx, _SCRIPT, "restart")
def stop(ctx: KnoeContext):
return run_script(ctx, _SCRIPT, "stop")
def status(ctx: KnoeContext):
return run_script(ctx, _SCRIPT, "status")

View File

@ -0,0 +1,31 @@
from __future__ import annotations
from ..context import KnoeContext
from ..legacy_shell import run_script
_SCRIPT = "init_garage_store.sh"
def initialize(ctx: KnoeContext):
return run_script(ctx, _SCRIPT, "initialize")
def start(ctx: KnoeContext):
return run_script(ctx, _SCRIPT, "start")
def update(ctx: KnoeContext):
return run_script(ctx, _SCRIPT, "update")
def restart(ctx: KnoeContext):
return run_script(ctx, _SCRIPT, "restart")
def stop(ctx: KnoeContext):
return run_script(ctx, _SCRIPT, "stop")
def status(ctx: KnoeContext):
return run_script(ctx, _SCRIPT, "status")

View File

@ -0,0 +1,31 @@
from __future__ import annotations
from ..context import KnoeContext
from ..legacy_shell import run_script
_SCRIPT = "init_openbao.sh"
def initialize(ctx: KnoeContext):
return run_script(ctx, _SCRIPT, "initialize")
def start(ctx: KnoeContext):
return run_script(ctx, _SCRIPT, "start")
def update(ctx: KnoeContext):
return run_script(ctx, _SCRIPT, "update")
def restart(ctx: KnoeContext):
return run_script(ctx, _SCRIPT, "restart")
def stop(ctx: KnoeContext):
return run_script(ctx, _SCRIPT, "stop")
def status(ctx: KnoeContext):
return run_script(ctx, _SCRIPT, "status")

View File

@ -0,0 +1,31 @@
from __future__ import annotations
from ..context import KnoeContext
from ..legacy_shell import run_script
_SCRIPT = "init_opentofu.sh"
def initialize(ctx: KnoeContext):
return run_script(ctx, _SCRIPT, "initialize")
def start(ctx: KnoeContext):
return run_script(ctx, _SCRIPT, "start")
def update(ctx: KnoeContext):
return run_script(ctx, _SCRIPT, "update")
def restart(ctx: KnoeContext):
return run_script(ctx, _SCRIPT, "restart")
def stop(ctx: KnoeContext):
return run_script(ctx, _SCRIPT, "stop")
def status(ctx: KnoeContext):
return run_script(ctx, _SCRIPT, "status")

View File

@ -0,0 +1,31 @@
from __future__ import annotations
from ..context import KnoeContext
from ..legacy_shell import run_script
_SCRIPT = "init_registry.sh"
def initialize(ctx: KnoeContext):
return run_script(ctx, _SCRIPT, "initialize")
def start(ctx: KnoeContext):
return run_script(ctx, _SCRIPT, "start")
def update(ctx: KnoeContext):
return run_script(ctx, _SCRIPT, "update")
def restart(ctx: KnoeContext):
return run_script(ctx, _SCRIPT, "restart")
def stop(ctx: KnoeContext):
return run_script(ctx, _SCRIPT, "stop")
def status(ctx: KnoeContext):
return run_script(ctx, _SCRIPT, "status")

29
knoe/ops/context.py Normal file
View File

@ -0,0 +1,29 @@
from __future__ import annotations
import os
import sys
from dataclasses import dataclass, field
from pathlib import Path
from typing import Callable
def _default_logger(msg: str) -> None:
print(msg)
def _default_err_logger(msg: str) -> None:
print(msg, file=sys.stderr)
@dataclass(slots=True)
class KnoeContext:
"""Small shared context object for Stage 1 ops modules."""
project_root: Path
cfg_path: Path | None = None
mode: str = "dev"
namespace: str = "default"
service_namespace: str = "default"
env: dict[str, str] = field(default_factory=lambda: dict(os.environ))
logger: Callable[[str], None] = _default_logger
err_logger: Callable[[str], None] = _default_err_logger

49
knoe/ops/files.py Normal file
View File

@ -0,0 +1,49 @@
from __future__ import annotations
import os
import shutil
from pathlib import Path
def ensure_dir(path: Path) -> None:
path.mkdir(parents=True, exist_ok=True)
def write_if_changed(path: Path, text: str, *, encoding: str = "utf-8") -> bool:
existing = None
try:
existing = path.read_text(encoding=encoding)
except Exception:
existing = None
if existing == text:
return False
ensure_dir(path.parent)
path.write_text(text, encoding=encoding)
return True
def copy_if_changed(src: Path, dst: Path) -> bool:
try:
if dst.exists() and src.read_bytes() == dst.read_bytes():
return False
except Exception:
pass
ensure_dir(dst.parent)
shutil.copy2(src, dst)
return True
def chmod_if_needed(path: Path, mode: int) -> bool:
try:
current = path.stat().st_mode & 0o777
except Exception:
return False
if current == mode:
return False
os.chmod(path, mode)
return True

49
knoe/ops/helm.py Normal file
View File

@ -0,0 +1,49 @@
from __future__ import annotations
import os
from typing import Mapping
from .context import KnoeContext
from .process import check
def _env(ctx: KnoeContext, extra_env: Mapping[str, str] | None = None) -> dict[str, str]:
e = dict(os.environ)
e.update(ctx.env)
if extra_env:
e.update(dict(extra_env))
return e
def repo_add(ctx: KnoeContext, name: str, url: str) -> None:
check(["helm", "repo", "add", name, url], env=_env(ctx))
def repo_update(ctx: KnoeContext) -> None:
check(["helm", "repo", "update"], env=_env(ctx))
def upgrade_install(
ctx: KnoeContext,
release: str,
chart: str,
*,
namespace: str | None = None,
values_file: str | None = None,
extra_args: list[str] | None = None,
) -> None:
cmd: list[str] = ["helm", "upgrade", "--install", release, chart]
if namespace:
cmd += ["--namespace", namespace]
if values_file:
cmd += ["-f", values_file]
if extra_args:
cmd += list(extra_args)
check(cmd, env=_env(ctx))
def uninstall(ctx: KnoeContext, release: str, *, namespace: str | None = None) -> None:
cmd: list[str] = ["helm", "uninstall", release]
if namespace:
cmd += ["--namespace", namespace]
check(cmd, env=_env(ctx))

95
knoe/ops/k8s.py Normal file
View File

@ -0,0 +1,95 @@
from __future__ import annotations
import json
import os
from typing import Any, Mapping, Sequence
from .context import KnoeContext
from .process import CommandFailed, check
def _env(ctx: KnoeContext, extra_env: Mapping[str, str] | None = None) -> dict[str, str]:
e = dict(os.environ)
e.update(ctx.env)
if extra_env:
e.update(dict(extra_env))
return e
def kubectl(
ctx: KnoeContext,
*args: str,
namespace: str | None = None,
input_text: str | None = None,
extra_env: Mapping[str, str] | None = None,
) -> str:
cmd: list[str] = ["kubectl", *args]
if namespace and ("-n" not in args and "--namespace" not in args):
cmd += ["-n", namespace]
proc = check(cmd, env=_env(ctx, extra_env), input_text=input_text)
return proc.stdout
def kubectl_json(ctx: KnoeContext, *args: str, namespace: str | None = None) -> Any:
out = kubectl(ctx, *args, "-o", "json", namespace=namespace)
return json.loads(out or "{}")
def namespace_exists(ctx: KnoeContext, namespace: str) -> bool:
try:
kubectl(ctx, "get", "namespace", namespace)
return True
except CommandFailed:
return False
def ensure_namespace(ctx: KnoeContext, namespace: str) -> None:
if namespace_exists(ctx, namespace):
return
kubectl(ctx, "create", "namespace", namespace)
def apply_yaml(ctx: KnoeContext, yaml_text: str, namespace: str | None = None) -> None:
kubectl(
ctx,
"apply",
"-f",
"-",
namespace=namespace,
input_text=yaml_text,
)
def get_resource_json(
ctx: KnoeContext,
resource: str,
name: str | None = None,
*,
namespace: str | None = None,
) -> Any:
args: list[str] = ["get", resource]
if name:
args.append(name)
return kubectl_json(ctx, *args, namespace=namespace)
def patch_resource(
ctx: KnoeContext,
resource: str,
name: str,
patch_json: str,
*,
namespace: str | None = None,
patch_type: str = "merge",
) -> None:
kubectl(
ctx,
"patch",
resource,
name,
"--type",
patch_type,
"-p",
patch_json,
namespace=namespace,
)

51
knoe/ops/legacy_shell.py Normal file
View File

@ -0,0 +1,51 @@
from __future__ import annotations
import os
from pathlib import Path
from typing import Callable, Mapping, Sequence
from .context import KnoeContext
from .process import check, run_streaming
def _env(ctx: KnoeContext, extra_env: Mapping[str, str] | None = None) -> dict[str, str]:
e = dict(os.environ)
e.update(ctx.env)
# Bridge common knobs for legacy scripts.
e.setdefault("PROLE_MODE", ctx.mode)
e.setdefault("PROLE_NAMESPACE", ctx.namespace)
e.setdefault("PROLE_SERVICE_NAMESPACE", ctx.service_namespace)
if ctx.cfg_path is not None:
e.setdefault("PROLE_CFG", str(ctx.cfg_path))
if extra_env:
e.update(dict(extra_env))
return e
def run_script(
ctx: KnoeContext,
script_name: str,
action: str,
*,
extra_args: Sequence[str] | None = None,
stdin_text: str | None = None,
on_line: Callable[[str], None] | None = None,
extra_env: Mapping[str, str] | None = None,
streaming: bool = True,
) -> int:
if not script_name.endswith(".sh"):
script_name = f"{script_name}.sh"
script_path = (ctx.project_root / "etc" / script_name).resolve()
cmd: list[str] = ["bash", str(script_path), action]
if extra_args:
cmd += list(extra_args)
env = _env(ctx, extra_env)
if streaming:
return run_streaming(cmd, cwd=ctx.project_root, env=env, stdin_text=stdin_text, on_line=on_line)
check(cmd, cwd=ctx.project_root, env=env, input_text=stdin_text)
return 0

93
knoe/ops/process.py Normal file
View File

@ -0,0 +1,93 @@
from __future__ import annotations
import subprocess
from dataclasses import dataclass
from pathlib import Path
from typing import Callable, Mapping, Sequence
@dataclass(slots=True)
class CommandFailed(RuntimeError):
cmd: Sequence[str]
returncode: int
stdout: str
stderr: str
def __str__(self) -> str: # pragma: no cover
cmd_str = " ".join(self.cmd)
return f"Command failed ({self.returncode}): {cmd_str}"
def run(
cmd: Sequence[str],
*,
cwd: Path | None = None,
env: Mapping[str, str] | None = None,
input_text: str | None = None,
timeout: float | None = None,
) -> subprocess.CompletedProcess[str]:
return subprocess.run(
list(cmd),
cwd=str(cwd) if cwd is not None else None,
env=dict(env) if env is not None else None,
input=input_text,
text=True,
capture_output=True,
timeout=timeout,
)
def check(
cmd: Sequence[str],
*,
cwd: Path | None = None,
env: Mapping[str, str] | None = None,
input_text: str | None = None,
timeout: float | None = None,
) -> subprocess.CompletedProcess[str]:
proc = run(cmd, cwd=cwd, env=env, input_text=input_text, timeout=timeout)
if proc.returncode != 0:
raise CommandFailed(cmd=cmd, returncode=proc.returncode, stdout=proc.stdout, stderr=proc.stderr)
return proc
def run_streaming(
cmd: Sequence[str],
*,
cwd: Path | None = None,
env: Mapping[str, str] | None = None,
stdin_text: str | None = None,
on_line: Callable[[str], None] | None = None,
) -> int:
"""Run a subprocess and stream stdout/stderr lines to `on_line`.
Returns the process return code.
"""
p = subprocess.Popen(
list(cmd),
cwd=str(cwd) if cwd is not None else None,
env=dict(env) if env is not None else None,
stdin=subprocess.PIPE if stdin_text is not None else None,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True,
bufsize=1,
)
try:
if stdin_text is not None and p.stdin is not None:
p.stdin.write(stdin_text)
p.stdin.close()
if p.stdout is not None:
for line in p.stdout:
if on_line is not None:
on_line(line.rstrip("\n"))
return p.wait()
finally:
try:
if p.stdout is not None:
p.stdout.close()
except Exception:
pass

52
knoe/ops/secrets.py Normal file
View File

@ -0,0 +1,52 @@
from __future__ import annotations
from typing import Any
from knoe import config as inst_config
from .context import KnoeContext
from .k8s import apply_yaml
def resolve_secret_value(value: str | None) -> str:
if not value:
return ""
return inst_config._resolve_secret_value(value)
def ensure_db_k8s_secrets(
ctx: KnoeContext,
*,
namespace: str,
name: str = "knoe-db",
username: str | None = None,
password: str | None = None,
extra: dict[str, str] | None = None,
) -> None:
"""Ensure a Secret exists containing DB credentials.
Stage 1 implementation is intentionally minimal and uses `kubectl apply`.
"""
data: dict[str, Any] = {}
if username is not None:
data["username"] = resolve_secret_value(username)
if password is not None:
data["password"] = resolve_secret_value(password)
if extra:
for k, v in extra.items():
data[k] = resolve_secret_value(v)
# We keep this as stringData for readability; kubectl will base64-encode.
yaml_lines = [
"apiVersion: v1",
"kind: Secret",
"metadata:",
f" name: {name}",
f" namespace: {namespace}",
"type: Opaque",
"stringData:",
]
for k, v in data.items():
yaml_lines.append(f" {k}: {v}")
apply_yaml(ctx, "\n".join(yaml_lines) + "\n", namespace=namespace)

15
knoe/ops/status.py Normal file
View File

@ -0,0 +1,15 @@
from __future__ import annotations
import time
from typing import Callable
def wait_for(predicate: Callable[[], bool], *, timeout_s: float = 60.0, poll_s: float = 1.0) -> bool:
"""Minimal shared readiness helper."""
end = time.time() + timeout_s
while time.time() < end:
if predicate():
return True
time.sleep(poll_s)
return False

View File

@ -193,16 +193,16 @@ def _default_base_cfg_text(env: str) -> str:
env = "dev" env = "dev"
# Keep defaults conservative and compatible with prior single-file defaults: # Keep defaults conservative and compatible with prior single-file defaults:
# - non-test uses `prole-db` to avoid surprising namespace drift # - non-test uses a stable DB namespace default
# - test is isolated # - test is isolated
if env == "test": if env == "test":
ns = "prole-test" ns = "prole-test"
sns = "prole-test" sns = "prole-test"
elif env == "dev": elif env == "dev":
ns = "prole-db" ns = "knoe-db"
sns = "default" sns = "default"
else: else:
ns = "prole-db" ns = "knoe-db"
sns = "knoe-system" sns = "knoe-system"
return ( return (

View File

@ -1,6 +1,6 @@
"""Backward-compatibility shim for legacy_tk imports.""" """Backward-compatibility shim for legacy_tk imports."""
from installer.ui import screens as _screens from knoe.ui import screens as _screens
globals().update({k: v for k, v in _screens.__dict__.items() if not k.startswith("__")}) globals().update({k: v for k, v in _screens.__dict__.items() if not k.startswith("__")})
__all__ = [name for name in globals() if not name.startswith("__")] __all__ = [name for name in globals() if not name.startswith("__")]

View File

@ -18,11 +18,11 @@ import tkinter as tk
import uuid import uuid
from tkinter import ttk, messagebox, filedialog from tkinter import ttk, messagebox, filedialog
from installer import deploy as inst_deploy from knoe import deploy as inst_deploy
from installer import prole_conf from knoe import prole_conf
from installer import screen as ui from knoe import screen as ui
from installer.build import get_build_command as inst_get_build_command from knoe.build import get_build_command as inst_get_build_command
from installer.config import ( from knoe.config import (
_expand_cfg_value, _expand_cfg_value,
_collect_cfg_vars, _collect_cfg_vars,
_is_prole_secret, _is_prole_secret,
@ -32,8 +32,8 @@ from installer.config import (
_encrypt_cfg_secret, _encrypt_cfg_secret,
_encrypt_prole_secret, _encrypt_prole_secret,
) )
from installer.core.controller import ProleController from knoe.core.controller import ProleController
from installer.core.env import ( from knoe.core.env import (
_detect_ansible_topology, _detect_ansible_topology,
_format_ansible_topology_summary, _format_ansible_topology_summary,
_normalize_k3s_token, _normalize_k3s_token,
@ -68,8 +68,8 @@ if platform.system() == "Darwin":
except Exception: except Exception:
pass pass
from installer.core.env import * # noqa: F401,F403 from knoe.core.env import * # noqa: F401,F403
from installer.core.actions import ( from knoe.core.actions import (
ProleInstaller, ProleInstaller,
ProleConsoleInstaller, ProleConsoleInstaller,
_prepare_k3s_pipeline, _prepare_k3s_pipeline,
@ -82,27 +82,27 @@ GLOBAL_SCAN_FRAMES: list = []
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Mixin imports — one per screen / concern # Mixin imports — one per screen / concern
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
from installer.ui.screens.base import ScreenBaseMixin from knoe.ui.screens.base import ScreenBaseMixin
from installer.ui.screens.navigation import NavigationMixin from knoe.ui.screens.navigation import NavigationMixin
from installer.ui.screens.welcome import WelcomeScreenMixin from knoe.ui.screens.welcome import WelcomeScreenMixin
from installer.ui.screens.dependencies import DependenciesScreenMixin from knoe.ui.screens.dependencies import DependenciesScreenMixin
from installer.ui.screens.network import NetworkScreenMixin from knoe.ui.screens.network import NetworkScreenMixin
from installer.ui.screens.environment import EnvironmentScreenMixin from knoe.ui.screens.environment import EnvironmentScreenMixin
from installer.ui.screens.database import DatabaseScreenMixin from knoe.ui.screens.database import DatabaseScreenMixin
from installer.ui.screens.database_options import DatabaseOptionsScreenMixin from knoe.ui.screens.database_options import DatabaseOptionsScreenMixin
from installer.ui.screens.cluster import ClusterScreenMixin from knoe.ui.screens.cluster import ClusterScreenMixin
from installer.ui.screens.cluster_nodes import ClusterNodesScreenMixin from knoe.ui.screens.cluster_nodes import ClusterNodesScreenMixin
from installer.ui.screens.services import ServicesScreenMixin from knoe.ui.screens.services import ServicesScreenMixin
from installer.ui.screens.security import SecurityScreenMixin from knoe.ui.screens.security import SecurityScreenMixin
from installer.ui.screens.supabase import SupabaseScreenMixin from knoe.ui.screens.supabase import SupabaseScreenMixin
from installer.ui.screens.gitops import GitOpsScreenMixin from knoe.ui.screens.gitops import GitOpsScreenMixin
from installer.ui.screens.argocd import ArgoCDScreenMixin from knoe.ui.screens.argocd import ArgoCDScreenMixin
from installer.ui.screens.docker import DockerScreenMixin from knoe.ui.screens.docker import DockerScreenMixin
from installer.ui.screens.build import BuildScreenMixin from knoe.ui.screens.build import BuildScreenMixin
from installer.ui.screens.packaging import PackagingScreenMixin from knoe.ui.screens.packaging import PackagingScreenMixin
from installer.ui.screens.deploy import DeployScreenMixin from knoe.ui.screens.deploy import DeployScreenMixin
from installer.ui.screens.validate import ValidateScreenMixin from knoe.ui.screens.validate import ValidateScreenMixin
from installer.ui.screens.cfg import ConfigMixin from knoe.ui.screens.cfg import ConfigMixin
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@ -651,7 +651,7 @@ class ProleInstaller(
try: try:
self.db_username = tk.StringVar(value=os.getlogin()) self.db_username = tk.StringVar(value=os.getlogin())
except Exception: except Exception:
self.db_username = tk.StringVar(value="prole-db") self.db_username = tk.StringVar(value="knoe")
self.db_password = tk.StringVar() self.db_password = tk.StringVar()
self.db_password_confirm = tk.StringVar() self.db_password_confirm = tk.StringVar()
self.namespace_owner = self._get_local_owner() self.namespace_owner = self._get_local_owner()
@ -856,7 +856,7 @@ def main():
else: else:
log_file_path = args.log log_file_path = args.log
from installer.config import setup_logging from knoe.config import setup_logging
setup_logging(verbose=args.verbose, debug=args.debug) setup_logging(verbose=args.verbose, debug=args.debug)
@ -867,7 +867,7 @@ def main():
) )
if args.command == "monitor": if args.command == "monitor":
from installer.core.monitor import run_monitor from knoe.core.monitor import run_monitor
pf_cfg = args.command_args[0] if args.command_args else None pf_cfg = args.command_args[0] if args.command_args else None
sys.exit(run_monitor(controller, args.config, pf_cfg, verbose=args.verbose)) sys.exit(run_monitor(controller, args.config, pf_cfg, verbose=args.verbose))
@ -921,12 +921,12 @@ def main():
print(f"Failed to start GUI: {e}", file=sys.stderr) print(f"Failed to start GUI: {e}", file=sys.stderr)
print("Falling back to ncurses interface...", file=sys.stderr) print("Falling back to ncurses interface...", file=sys.stderr)
time.sleep(1) time.sleep(1)
from installer.ncurses_installer import run_ncurses_installer from knoe.ncurses_installer import run_ncurses_installer
run_ncurses_installer(controller) run_ncurses_installer(controller)
else: else:
_ensure_ansible_vault_credentials(prompt_ui=False) _ensure_ansible_vault_credentials(prompt_ui=False)
from installer.ncurses_installer import run_ncurses_installer from knoe.ncurses_installer import run_ncurses_installer
run_ncurses_installer(controller) run_ncurses_installer(controller)

View File

@ -11,8 +11,8 @@ import subprocess
import threading import threading
import tkinter as tk import tkinter as tk
from installer import screen as ui from knoe import screen as ui
from installer.core.env import PROJECT_ROOT, _normalize_cluster_env from knoe.core.env import PROJECT_ROOT, _normalize_cluster_env
class ArgoCDScreenMixin: class ArgoCDScreenMixin:
@ -207,7 +207,7 @@ class ArgoCDScreenMixin:
if candidate.exists(): if candidate.exists():
cfg_path = candidate cfg_path = candidate
if not cfg_path: if not cfg_path:
from installer import prole_conf from knoe import prole_conf
candidate = prole_conf.entrypoint_path( candidate = prole_conf.entrypoint_path(
prole_conf.resolve_prole_conf_dir(PROJECT_ROOT) prole_conf.resolve_prole_conf_dir(PROJECT_ROOT)

View File

@ -5,8 +5,8 @@ import subprocess
from datetime import datetime from datetime import datetime
import tkinter as tk import tkinter as tk
from tkinter import ttk, messagebox, filedialog from tkinter import ttk, messagebox, filedialog
from installer.screen import TerminalConsole from knoe.screen import TerminalConsole
from installer import screen as ui from knoe import screen as ui
class ScreenBaseMixin: class ScreenBaseMixin:

View File

@ -11,11 +11,11 @@ import time
import webbrowser import webbrowser
import tkinter as tk import tkinter as tk
from tkinter import ttk, messagebox, filedialog from tkinter import ttk, messagebox, filedialog
from installer.screen import TerminalConsole from knoe.screen import TerminalConsole
from installer import deploy as inst_deploy from knoe import deploy as inst_deploy
from installer.build import get_build_command as inst_get_build_command from knoe.build import get_build_command as inst_get_build_command
from installer import screen as ui from knoe import screen as ui
from installer.core.env import PROJECT_ROOT from knoe.core.env import PROJECT_ROOT
class BuildScreenMixin: class BuildScreenMixin:
@ -793,7 +793,7 @@ echo "-------------------------------------------------------------------";
pass pass
def get_build_command(self, env: str) -> str: def get_build_command(self, env: str) -> str:
"""Delegate to installer.build.get_build_command.""" """Delegate to knoe.build.get_build_command."""
return inst_get_build_command(PROJECT_ROOT, env) return inst_get_build_command(PROJECT_ROOT, env)
# ---------------- Build Summary page ---------------- # ---------------- Build Summary page ----------------

View File

@ -9,8 +9,8 @@ import subprocess
from pathlib import Path 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 installer import prole_conf from knoe import prole_conf
from installer.core.env import ( from knoe.core.env import (
DEFAULT_ACTION_FLAGS, DEFAULT_ACTION_FLAGS,
DEFAULT_OLLAMA_PORT, DEFAULT_OLLAMA_PORT,
PROJECT_ROOT, PROJECT_ROOT,
@ -28,7 +28,7 @@ from installer.core.env import (
_read_k3s_cfg, _read_k3s_cfg,
_render_prole_cfg, _render_prole_cfg,
) )
from installer.config import _collect_cfg_vars, _encrypt_cfg_secret, _expand_cfg_value from knoe.config import _collect_cfg_vars, _encrypt_cfg_secret, _expand_cfg_value
class ConfigMixin: class ConfigMixin:

View File

@ -12,10 +12,10 @@ import urllib.request
from pathlib import Path 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 installer.screen import TerminalConsole from knoe.screen import TerminalConsole
from installer import screen as ui from knoe import screen as ui
from installer import prole_conf from knoe import prole_conf
from installer.core.env import ( from knoe.core.env import (
PROJECT_ROOT, PROJECT_ROOT,
_default_opentofu_pipeline_url, _default_opentofu_pipeline_url,
_deployment_mode_from_env, _deployment_mode_from_env,
@ -29,12 +29,14 @@ from installer.core.env import (
_resolve_k3s_connection as _resolve_k3s_connection_fn, _resolve_k3s_connection as _resolve_k3s_connection_fn,
_safe_str, _safe_str,
) )
from installer.config import ( from knoe.config import (
_collect_cfg_vars_from_data,
_encrypt_cfg_secret, _encrypt_cfg_secret,
_expand_path_expr,
_merge_kubeconfig, _merge_kubeconfig,
_write_k3s_kubeconfig, _write_k3s_kubeconfig,
) )
from installer.core.actions import _reset_k3s_namespace from knoe.core.actions import _reset_k3s_namespace
class ClusterScreenMixin: class ClusterScreenMixin:
@ -1386,13 +1388,15 @@ class ClusterScreenMixin:
"Validation", "Please specify an artifact staging directory." "Validation", "Please specify an artifact staging directory."
) )
return False return False
p = Path(path).expanduser() cfg_vars = _collect_cfg_vars_from_data(getattr(self, "prole_cfg_data", None))
expanded_path = _expand_path_expr(path, cfg_vars)
p = Path(expanded_path).expanduser()
if not p.exists(): if not p.exists():
try: try:
p.mkdir(parents=True, exist_ok=True) p.mkdir(parents=True, exist_ok=True)
except Exception as e: except Exception as e:
messagebox.showerror( messagebox.showerror(
"Error", f"Failed to create directory {path}: {e}" "Error", f"Failed to create directory {expanded_path or path}: {e}"
) )
return False return False
cluster_val = self.cluster_env.get() cluster_val = self.cluster_env.get()

View File

@ -6,7 +6,7 @@ import json
import tkinter as tk import tkinter as tk
from tkinter import messagebox from tkinter import messagebox
from installer import screen as ui from knoe import screen as ui
class ClusterNodesScreenMixin: class ClusterNodesScreenMixin:

View File

@ -3,15 +3,14 @@
import json import json
import os import os
import re import re
import shutil
import subprocess import subprocess
import threading import threading
from pathlib import Path from pathlib import Path
import tkinter as tk import tkinter as tk
from tkinter import ttk, messagebox, filedialog, simpledialog from tkinter import ttk, messagebox, simpledialog
from installer import screen as ui from knoe import screen as ui
from installer.config import get_docker_build_platform_args from knoe.config import get_docker_build_platform_args
from installer.core.env import ( from knoe.core.env import (
PROJECT_ROOT, PROJECT_ROOT,
_bool_str, _bool_str,
_deployment_mode_from_env, _deployment_mode_from_env,
@ -20,6 +19,7 @@ from installer.core.env import (
_push_docker_image, _push_docker_image,
get_resource_path, get_resource_path,
) )
from knoe.core.build_context import copy_build_context_dir
class DatabaseScreenMixin: class DatabaseScreenMixin:
@ -396,7 +396,7 @@ class DatabaseScreenMixin:
if vals: if vals:
ns = vals[1] ns = vals[1]
if ns == "supabase": if ns == "supabase":
# Do not allow selecting 'supabase' as the primary prole-db namespace # Do not allow selecting 'supabase' as the primary knoe-db namespace
return return
if ns == self.db_namespace.get(): if ns == self.db_namespace.get():
# Already selected, don't trigger re-refresh/save # Already selected, don't trigger re-refresh/save
@ -562,14 +562,14 @@ class DatabaseScreenMixin:
if ns == "supabase": if ns == "supabase":
port = "5432" port = "5432"
else: else:
# For prole-db namespaces # For knoe-db namespaces
if ns == current_ns: if ns == current_ns:
if supabase_enabled: if supabase_enabled:
port = "15432" port = "15432"
else: else:
port = db_host_port or "5432" port = db_host_port or "5432"
else: else:
# If it's another prole-db namespace, we might not know its port easily # If it's another knoe-db namespace, we might not know its port easily
# but if Supabase is enabled globally, we assume standard alternate port # but if Supabase is enabled globally, we assume standard alternate port
if supabase_enabled: if supabase_enabled:
port = "15432" port = "15432"
@ -940,7 +940,7 @@ class DatabaseScreenMixin:
self._render_title("Build Database Image", y=150) self._render_title("Build Database Image", y=150)
self._render_paragraph( self._render_paragraph(
"Building the prole-db Postgres image. This may take a few minutes.", y=200 "Building the knoe-db Postgres image. This may take a few minutes.", y=200
) )
# Registry status (checked async) # Registry status (checked async)
@ -1414,7 +1414,7 @@ class DatabaseScreenMixin:
) )
tag = self.get_prole_db_version() tag = self.get_prole_db_version()
image_name = f"prole-db:{tag}" image_name = f"knoe-db:{tag}"
# Use $HOME/.prole/build for Docker build context # Use $HOME/.prole/build for Docker build context
# This avoids issues with PyInstaller's temporary _MEIPASS directory # This avoids issues with PyInstaller's temporary _MEIPASS directory
@ -1424,14 +1424,7 @@ class DatabaseScreenMixin:
# Copy prole-db directory to writable location # Copy prole-db directory to writable location
source_dir = get_resource_path("prole-db") source_dir = get_resource_path("prole-db")
if source_dir.exists(): copy_build_context_dir(source_dir, build_dir)
import shutil
# Remove old build dir and copy fresh
if source_dir.resolve() != build_dir.resolve():
if build_dir.exists():
shutil.rmtree(build_dir)
shutil.copytree(source_dir, build_dir)
cwd = build_dir cwd = build_dir
@ -1498,7 +1491,7 @@ class DatabaseScreenMixin:
import_tag = image_name import_tag = image_name
if registry_info: if registry_info:
host_registry, cluster_registry = registry_info host_registry, cluster_registry = registry_info
remote_tag = f"{host_registry}/prole-db:{tag}" remote_tag = f"{host_registry}/knoe-db:{tag}"
self._db_build_console.write( self._db_build_console.write(
f"Tagging image for registry: {remote_tag}\n" f"Tagging image for registry: {remote_tag}\n"
) )
@ -1578,13 +1571,13 @@ class DatabaseScreenMixin:
self._db_build_console.write(f"Push complete: {remote_tag}\n") self._db_build_console.write(f"Push complete: {remote_tag}\n")
if cluster_registry: if cluster_registry:
import_tag = f"{cluster_registry}/prole-db:{tag}" import_tag = f"{cluster_registry}/knoe-db:{tag}"
# k3d registry is often reachable internally without the .localhost suffix # k3d registry is often reachable internally without the .localhost suffix
if cluster_registry.endswith(".localhost:5000"): if cluster_registry.endswith(".localhost:5000"):
alt_registry = cluster_registry.replace( alt_registry = cluster_registry.replace(
".localhost", "" ".localhost", ""
) )
alt_tag = f"{alt_registry}/prole-db:{tag}" alt_tag = f"{alt_registry}/knoe-db:{tag}"
if alt_tag != import_tag: if alt_tag != import_tag:
self._db_build_console.write( self._db_build_console.write(
f"Tagging image for k3d import: {alt_tag}\n" f"Tagging image for k3d import: {alt_tag}\n"
@ -1647,7 +1640,7 @@ class DatabaseScreenMixin:
) )
return return
remote_tag = f"{registry_url}/prole-db:{tag}" remote_tag = f"{registry_url}/knoe-db:{tag}"
self._db_build_console.write( self._db_build_console.write(
f"Tagging image for registry: {remote_tag}\n" f"Tagging image for registry: {remote_tag}\n"
) )

View File

@ -7,8 +7,8 @@ import threading
from pathlib import Path from pathlib import Path
import tkinter as tk import tkinter as tk
from tkinter import ttk, messagebox from tkinter import ttk, messagebox
from installer import screen as ui from knoe import screen as ui
from installer.core.env import PROJECT_ROOT, _safe_str from knoe.core.env import PROJECT_ROOT, _safe_str
class DatabaseOptionsScreenMixin: class DatabaseOptionsScreenMixin:
"""Mixin for the Database Options screen.""" """Mixin for the Database Options screen."""

View File

@ -5,8 +5,8 @@ import threading
import time import time
import tkinter as tk import tkinter as tk
from tkinter import ttk, messagebox, filedialog from tkinter import ttk, messagebox, filedialog
from installer import config as inst_config from knoe import config as inst_config
from installer import screen as ui from knoe import screen as ui
class DependenciesScreenMixin: class DependenciesScreenMixin:
@ -627,9 +627,9 @@ class DependenciesScreenMixin:
return True return True
def get_dep_info(self, dep): def get_dep_info(self, dep):
"""Delegate dependency probing to installer.config.get_dep_info.""" """Delegate dependency probing to knoe.config.get_dep_info."""
return inst_config.get_dep_info(dep) return inst_config.get_dep_info(dep)
def normalize_version(self, text: str) -> str: def normalize_version(self, text: str) -> str:
"""Normalize versions via installer.config.normalize_version.""" """Normalize versions via knoe.config.normalize_version."""
return inst_config.normalize_version(text) return inst_config.normalize_version(text)

View File

@ -8,8 +8,8 @@ import threading
from pathlib import Path 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 installer import screen as ui from knoe import screen as ui
from installer.core.env import PROJECT_ROOT from knoe.core.env import PROJECT_ROOT
class DeployScreenMixin: class DeployScreenMixin:

View File

@ -9,8 +9,8 @@ import sys
import time import time
from pathlib import Path from pathlib import Path
from installer.config import get_docker_build_platform_args from knoe.config import get_docker_build_platform_args
from installer.core.env import ( from knoe.core.env import (
PROJECT_ROOT, PROJECT_ROOT,
_collect_images_from_files, _collect_images_from_files,
_deployment_mode_from_env, _deployment_mode_from_env,
@ -424,10 +424,10 @@ class DockerScreenMixin:
return None return None
def build_docker_image(self): def build_docker_image(self):
"""Build prole-db Docker image""" """Build knoe-db Docker image"""
# If Kerberos is enabled, update pg_hba.conf in conf/postgresql before copying # If Kerberos is enabled, update pg_hba.conf in conf/postgresql before copying
if self.kerberos_enabled.get(): if self.kerberos_enabled.get():
realm = self.kerberos_realm.get().strip() or "EXAMPLE.COM" realm = self.kerberos_realm.get().strip() or "KNOEY.COM"
hba_src = PROJECT_ROOT / "conf" / "postgresql" / "pg_hba.conf" hba_src = PROJECT_ROOT / "conf" / "postgresql" / "pg_hba.conf"
if hba_src.exists(): if hba_src.exists():
content = hba_src.read_text() content = hba_src.read_text()
@ -436,14 +436,14 @@ class DockerScreenMixin:
content += f"\nhost all all all gss include_realm=1 krb_realm={realm}\n" content += f"\nhost all all all gss include_realm=1 krb_realm={realm}\n"
else: else:
content = content.replace( content = content.replace(
"krb_realm=EXAMPLE.COM", f"krb_realm={realm}" "krb_realm=KNOEY.COM", f"krb_realm={realm}"
) )
hba_src.write_text(content) hba_src.write_text(content)
print(f"Updated {hba_src} with realm {realm}") print(f"Updated {hba_src} with realm {realm}")
# Base local image tag (before pushing to registry) # Base local image tag (before pushing to registry)
version = self.get_prole_db_version() version = self.get_prole_db_version()
image_tag = f"prole-db:{version}" image_tag = f"knoe-db:{version}"
# Prepare build context: copy conf/postgresql to prole-db/postgresql # Prepare build context: copy conf/postgresql to prole-db/postgresql
conf_src = PROJECT_ROOT / "conf" / "postgresql" conf_src = PROJECT_ROOT / "conf" / "postgresql"
@ -514,9 +514,9 @@ class DockerScreenMixin:
return return
version = self.get_prole_db_version() version = self.get_prole_db_version()
registry = getattr(self, "registry_url", "localhost:5000") registry = getattr(self, "registry_url", "localhost:5000")
image_name = f"prole-db:{version}" image_name = f"knoe-db:{version}"
image = getattr(self, "local_image_tag", image_name) image = getattr(self, "local_image_tag", image_name)
self.remote_image_tag = f"{registry}/prole-db:{version}" self.remote_image_tag = f"{registry}/knoe-db:{version}"
subprocess.run( subprocess.run(
["docker", "tag", image, self.remote_image_tag], ["docker", "tag", image, self.remote_image_tag],
check=True, check=True,
@ -537,7 +537,7 @@ class DockerScreenMixin:
def import_k3d_image(self, cluster_name="knoe-dev-cluster"): def import_k3d_image(self, cluster_name="knoe-dev-cluster"):
"""Import image to k3d cluster (only for Dev).""" """Import image to k3d cluster (only for Dev)."""
version = self.get_prole_db_version() version = self.get_prole_db_version()
image_name = f"prole-db:{version}" image_name = f"knoe-db:{version}"
subprocess.run( subprocess.run(
["k3d", "image", "import", image_name, "-c", cluster_name], ["k3d", "image", "import", image_name, "-c", cluster_name],
check=True, check=True,
@ -556,12 +556,12 @@ class DockerScreenMixin:
if not manifest_path.exists(): if not manifest_path.exists():
continue continue
content = manifest_path.read_text() content = manifest_path.read_text()
# Update imageName: prole-db:17.7-043 # Update imageName: knoe-db:17.7-043
new_content = re.sub(r"imageName:\s*.*", f"imageName: {image_tag}", content) new_content = re.sub(r"imageName:\s*.*", f"imageName: {image_tag}", content)
# Update Kerberos realm in manifest if enabled # Update Kerberos realm in manifest if enabled
if self.kerberos_enabled.get(): if self.kerberos_enabled.get():
realm = self.kerberos_realm.get().strip() or "EXAMPLE.COM" realm = self.kerberos_realm.get().strip() or "KNOEY.COM"
if "gss" not in new_content: if "gss" not in new_content:
# Insert GSS rule after existing scram rules for service accounts # Insert GSS rule after existing scram rules for service accounts
new_content = new_content.replace( new_content = new_content.replace(
@ -571,7 +571,7 @@ class DockerScreenMixin:
) )
else: else:
new_content = new_content.replace( new_content = new_content.replace(
"krb_realm=EXAMPLE.COM", f"krb_realm={realm}" "krb_realm=KNOEY.COM", f"krb_realm={realm}"
) )
if new_content != content: if new_content != content:

View File

@ -7,8 +7,8 @@ import subprocess
from pathlib import Path 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 installer import screen as ui from knoe import screen as ui
from installer.core.env import PROJECT_ROOT, _expand_cfg_value from knoe.core.env import PROJECT_ROOT, _expand_cfg_value
class EnvironmentScreenMixin: class EnvironmentScreenMixin:

View File

@ -7,9 +7,9 @@ import subprocess
import threading import threading
import tkinter as tk import tkinter as tk
from installer import prole_conf from knoe import prole_conf
from installer import screen as ui from knoe import screen as ui
from installer.core.env import PROJECT_ROOT, _normalize_cluster_env, _parse_bool from knoe.core.env import PROJECT_ROOT, _normalize_cluster_env, _parse_bool
class GitOpsScreenMixin: class GitOpsScreenMixin:

View File

@ -5,8 +5,8 @@ import platform
from pathlib import Path 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 installer import config as inst_config from knoe import config as inst_config
from installer.core.env import get_resource_path, _deployment_mode_from_env from knoe.core.env import get_resource_path, _deployment_mode_from_env
class NavigationMixin: class NavigationMixin:

Some files were not shown because too many files have changed in this diff Show More