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)
@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'
from installer.core.controller import ProleController
from installer.core.env import PROJECT_ROOT
from knoe.core.controller import ProleController
from knoe.core.env import PROJECT_ROOT
from prole.deployment import ProleDeployment
import sys, os
from pathlib import Path
@ -102,5 +102,5 @@ pyconv:
clean:
@echo "Cleaning build artifacts..."
rm -rf $(BUILD_DIR) $(DIST_DIR) *.spec
rm -rf __pycache__ prole/__pycache__ installer/__pycache__
rm -rf __pycache__ prole/__pycache__ knoe/__pycache__
@echo "✓ Clean complete"

View File

@ -1,10 +1,10 @@
; 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.
[User]
; 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]
; Screen-scoped inputs used for unattended replays (-S)
@ -37,51 +37,58 @@ dependencies.python.install = true
dependencies.verify_all = false
disk_selection.disk_type = local
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.at_rest_encryption_enabled = true
init_cluster.cluster_env = service
init_cluster.deployment_target = prole-service-cluster
init_cluster.cluster_env = dev
init_cluster.deployment_target = prole-dev-cluster
init_cluster.gitops_enabled = false
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.mode = k3s
init_cluster.mode = k3d
init_cluster.start_cluster = true
init_cluster.supabase_enabled = false
init_cnpg_deploy.force_rollout = false
init_cnpg_deploy.run_deploy = true
init_db_build.run_build = true
init_password.db_password =
init_password.db_password_confirm =
init_password.db_host_port = 5432
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.generate_ssh_key = true
init_scripts.run_scripts = true
kerberos_config.enabled = true
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.test_connection = false
kerberos_config.user = administrator
network_scan.run = true
ollama_config.model =
ollama_config.server_host =
ollama_config.server_host = fairyland.prole.org
ollama_config.server_port = 11434
[Global]
; Variables used by name in more than one place or assumed global scope
CLUSTER_ENV = service
CLUSTER_ENV = dev
DB_HOST_PORT = 5432
DB_PASSWORD =
DEPLOYMENT_MODE = k3s
DEPLOYMENT_TARGET = prole-service-cluster
DB_PASSWORD = ${PROLE_SECRET:v1:dsPAIoTIWP8_8O35:RfNqyfh0j_eu8-tt9LZBQ_06wLa8D8aW}
DEPLOYMENT_MODE = k3d
DEPLOYMENT_TARGET = prole-dev-cluster
DOCKER_PRELOAD = false
KUBECONTEXT = prole-k3s
NAMESPACE = knoe-db
OPTIONAL_WORKLOADS_MIN_READY_SCHEDULABLE_NODES = 2
PROLE_DB_USER = root
PROLE_HOME = /Users/chrisfu/dev/prole
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
SERVICE_NAMESPACE = knoe-system
@ -120,7 +127,10 @@ PROLE_SERVICE = /Users/chrisfu/dev/prole/etc
; No configuration values captured yet for this section.
[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]
AT_REST_ENCRYPTION_ENABLED = True
@ -135,21 +145,21 @@ SUPABASE_ENABLED = False
; No configuration values captured yet for this section.
[Initialize Cluster]
ENVIRONMENT = service
ENVIRONMENT = dev
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)]
CLUSTER_ENV = dev
DISPLAY_NAME = knoe-dev-cluster
KUBECTL_CONTEXT = prole-k3s
KUBECTL_CONTEXT =
MODE = k3d
[Service Cluster (k3s)]
CLUSTER_ENV = prole-service-cluster
DISPLAY_NAME = prole-service-cluster
K3S_SERVER_URL = https://myrddin.prole.org:6443
K3S_TOKEN = ${PROLE_SECRET:v1:tOW53iZqoGRwqxUz:n0jktIyKBQ5cAggXfdR2oZ7OYyt859dRFHn5f4MzCy2zL7D_Ur9C-4e89RrTNPVLlGmd2GbHnP9uSLfnxRKe4zmypr5CfQnj3WDpwTr977EZBckkGFaSUQvUq-nShA-mRNDj14bXc2s5Oba9IQ9tiYRj4HT0W32MQ04HzQ==}
K3S_TOKEN = ${PROLE_SECRET:v1:oJzjBIw61NsNXh7f:DJ9niYssxFCc6-gubQmWNlmQkhUhOtxLaXIIOTGpSxkfYK1uxcLw_BWb8n6kkl7uDkM1jEc4isjwjbycG5hRdd2at0oILHcyyFwrICH1roH4o0MHdd-kyxKkWo23WgSnTUGdRXfch5UWyPYZKniCxSbBhWud4ZFIZsZ4pw==}
MODE = k3s
PIPELINE_URL = http://127.0.0.1:8080
@ -161,14 +171,15 @@ MODE = k8s
PIPELINE_URL = http://127.0.0.1:8080
[Docker Build]
; No configuration values captured yet for this section.
LOCAL_REGISTRY = localhost:5000
LOCAL_REGISTRY_INTERNAL = k3d-prole-registry.localhost:5000
[Initialization Scripts]
; No configuration values captured yet for this section.
[Deployment]
MODE = k3s
TARGET = prole-service-cluster
MODE = k3d
TARGET = prole-dev-cluster
[Install]
; No configuration values captured yet for this section.

View File

@ -1,3 +1,11 @@
# Port mappings for Prole Tools (generated).
# 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
; 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.
[User]
@ -50,7 +50,7 @@ init_cluster.cluster_env = prod
init_cluster.deployment_target = prole-prod-cluster
init_cluster.gitops_enabled = false
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.mode = k8s
init_cluster.start_cluster = true
@ -67,7 +67,7 @@ init_password.generate_ssh_key = true
init_scripts.run_scripts = true
kerberos_config.enabled = true
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.test_connection = false
kerberos_config.user = administrator
@ -89,7 +89,7 @@ OPTIONAL_WORKLOADS_MIN_READY_SCHEDULABLE_NODES = 2
PROLE_DB_USER = root
PROLE_HOME = $HOME/dev/prole
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
SERVICE_NAMESPACE = knoe-system
@ -155,7 +155,7 @@ MODE = k3d
CLUSTER_ENV = prole-service-cluster
DISPLAY_NAME = prole-service-cluster
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
PIPELINE_URL = http://127.0.0.1:8080

View File

@ -1,5 +1,5 @@
; 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.
[User]
@ -50,7 +50,7 @@ init_cluster.cluster_env = service
init_cluster.deployment_target = prole-service-cluster
init_cluster.gitops_enabled = false
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.mode = k3s
init_cluster.start_cluster = true
@ -67,7 +67,7 @@ init_password.generate_ssh_key = true
init_scripts.run_scripts = true
kerberos_config.enabled = true
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.test_connection = false
kerberos_config.user = administrator
@ -84,13 +84,12 @@ DB_PASSWORD =
DEPLOYMENT_MODE = k3s
DEPLOYMENT_TARGET = prole-service-cluster
DOCKER_PRELOAD = false
KUBECONTEXT = prole-k3s
NAMESPACE = prole-db
OPTIONAL_WORKLOADS_MIN_READY_SCHEDULABLE_NODES = 2
PROLE_DB_USER = root
PROLE_HOME = $HOME/dev/prole
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
SERVICE_NAMESPACE = knoe-system
@ -135,10 +134,7 @@ OLLAMA_SERVER_HOST = fairyland.prole.org
OLLAMA_SERVER_PORT = 11434
[Optional Features]
AT_REST_ENCRYPTION_ENABLED = True
GITOPS_ENABLED = False
KERBEROS_ENABLED = True
SUPABASE_ENABLED = False
[GitOps]
; No configuration values captured yet for this section.
@ -147,14 +143,12 @@ SUPABASE_ENABLED = False
; No configuration values captured yet for this section.
[Initialize Cluster]
ENVIRONMENT = service
K3S_SERVER_URL = https://myrddin.prole.org:6443
K3S_TOKEN = ${PROLE_SECRET:v1:570CIZjbSCH131v_:pDQwbH9nKYsCl5z0f-iAb-cilZlo1KLpI16c6Hgj0Ic81Jk1I7CSjEFo5UJ1ezV_pdat-7kxhaWE_TxCXG6ZnuOA8SBh3xCCi1LsgYFluPwm47b4IaS5ftvMOe5TVWbQfnzsXuU4SdO_3O9mgagKHdDIzu8kufYfpeDyqA==}
; No configuration values captured yet for this section.
[Dev Cluster (k3d)]
CLUSTER_ENV = dev
DISPLAY_NAME = knoe-dev-cluster
KUBECTL_CONTEXT = prole-k3s
KUBECTL_CONTEXT =
MODE = k3d
[Service Cluster (k3s)]

View File

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

View File

@ -14,6 +14,119 @@ spec:
labels:
app: prole-auth
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:
- name: prole-auth
image: prole-auth:latest
@ -37,11 +150,74 @@ spec:
key: servicePrincipal
- name: PROLE_KERBEROS_KEYTAB_PATH
value: "/etc/prole/keytabs/http.keytab"
- name: PROLE_KERBEROS_REALM
valueFrom:
configMapKeyRef:
name: prole-auth-kerberos
key: realm
volumeMounts:
- name: keytab
mountPath: /etc/prole/keytabs
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:
- name: keytab
emptyDir: {}
- name: keytab-secret
secret:
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:
# Kerberos HTTP service principal for SPNEGO (must match keytab)
servicePrincipal: "HTTP/api.prole.org@PROLE.ORG"
realm: "PROLE.ORG"

View File

@ -11,4 +11,24 @@ spec:
- name: http
port: 8080
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

View File

@ -4,7 +4,7 @@ metadata:
name: prole-db
spec:
instances: 3
imageName: myrddin.prole.org:5000/prole-db:0.0.0
imageName: k3d-prole-registry:5000/knoe-db:17-137
postgresUID: 100
postgresGID: 101
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/`:
- `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`
Required (provided externally):
- Secret `prole-auth-secrets` with key `sessionSecret`
- 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

View File

@ -82,14 +82,14 @@ The ncurses implementation shares the same business logic (`ProleController`) as
**Key Components:**
1. **installer/ncurses_ui.py** - UI primitives
1. **knoe/ncurses_ui.py** - UI primitives
- `CursesWindow` - Basic window wrapper with rendering helpers
- `TerminalConsole` - Scrollable console output
- `NavFooter` - Navigation button bar
- `InputField` - Text input 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
- `run_ncurses_installer()` - Entry point for ncurses mode
@ -145,7 +145,7 @@ Test the ncurses interface:
```bash
# 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
./install.py --no-gui

View File

@ -2,7 +2,7 @@
set -euo pipefail
# 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)"
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
@ -40,7 +40,7 @@ done
cd "$PROJECT_ROOT/prole-db"
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
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
BUILD_ARGS+=(-t "$TAG")
else
BUILD_ARGS+=(-t "prole-db:$TAG")
BUILD_ARGS+=(-t "knoe-db:$TAG")
fi
if [[ "$PUSH" == "true" ]]; then
log "Pushing to registry..."
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
BUILD_ARGS+=(-t "prole-db:latest")
BUILD_ARGS+=(-t "knoe-db:latest")
fi
else
# 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
BUILD_ARGS+=(-t "$TAG")
else
BUILD_ARGS+=(-t "prole-db:$TAG")
BUILD_ARGS+=(-t "knoe-db:$TAG")
fi
BUILD_ARGS+=(--load)
fi
@ -82,5 +82,5 @@ if docker buildx version >/dev/null 2>&1; then
docker buildx build --progress=plain "${BUILD_ARGS[@]}" .
else
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

View File

@ -506,7 +506,7 @@ get_latest_image() {
release=$(printf "%03d" "$release")
fi
echo "prole-db:${pg_version}-${release}"
echo "knoe-db:${pg_version}-${release}"
}
resolve_cnpg_image() {
@ -585,7 +585,7 @@ resolve_cnpg_image() {
# If the image is unqualified (no registry), prefix it with the chosen registry
if [[ "$image" != */* ]]; then
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
image="${registry}/${image}"
fi
@ -1252,7 +1252,7 @@ _push_to_k3d_registry() {
local image="$1"
local cluster_name="$2"
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
local push_ref="${push_host}/${plain_image}"
@ -1272,7 +1272,7 @@ _push_to_k3d_registry() {
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.
# Steps: containerd cache → Docker daemon (registry tag) → Docker daemon (plain tag)
# → tar import → docker build + push/import.
@ -1289,12 +1289,12 @@ _ensure_prole_db_image() {
if [[ "$VERSION" == "latest" || -z "$VERSION" ]]; then
image=$(get_latest_image)
else
image="prole-db:$VERSION"
image="knoe-db:$VERSION"
fi
fi
image=$(resolve_cnpg_image "$image")
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
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
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
fi
if [[ "${PROLE_MODE:-}" == "k3s" ]]; then
@ -1388,7 +1388,7 @@ _ensure_prole_db_image() {
else
echo " Image '$image' not found in k3d, Docker daemon, or docker-import dir."
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
echo "ERROR: docker build failed for image '$plain_image'." >&2
return 1
@ -1427,7 +1427,7 @@ ensure_prole_stack_resources() {
if [[ "$VERSION" == "latest" || -z "$VERSION" ]]; then
image=$(get_latest_image)
else
image="prole-db:$VERSION"
image="knoe-db:$VERSION"
fi
fi
image=$(resolve_cnpg_image "$image")
@ -2361,7 +2361,7 @@ restore_cnpg_cluster() {
if [[ "$VERSION" == "latest" || -z "$VERSION" ]]; then
image=$(get_latest_image)
else
image="prole-db:$VERSION"
image="knoe-db:$VERSION"
fi
image=$(resolve_cnpg_image "$image")
@ -2458,57 +2458,43 @@ bao_service_url() {
}
fetch_admin_keys_and_db_pass_from_bao_or_local() {
local token url
if [[ -f "$OPENBAO_TOKEN_FILE" ]]; then
token=$(cat "$OPENBAO_TOKEN_FILE")
else
token=""
fi
url=$(bao_service_url)
if [[ -n "$token" && -n "$url" ]]; then
local priv_b64 pub_b64
priv_b64=$(fetch_openbao_secret "$BAO_PATH_ADMIN" "admin_private_key_b64")
pub_b64=$(fetch_openbao_secret "$BAO_PATH_ADMIN" "admin_public_key_b64")
if [[ -n "${priv_b64:-}" && "${priv_b64:-}" != "null" && -n "${pub_b64:-}" && "${pub_b64:-}" != "null" ]]; then
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 ..."
if curl -sS -H "X-Vault-Token: $token" "$url/v1/kv/data/$BAO_PATH_DB" | jq -e '.data.data' >/dev/null 2>&1; then
local db_pass
db_pass=$(curl -sS -H "X-Vault-Token: $token" "$url/v1/kv/data/$BAO_PATH_DB" | jq -r '.data.data.password')
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"
# Prefer the generic filenames.
printf "%s" "$priv_b64" | base64 -d >"$ADMIN_PRIV_GENERIC"
printf "%s" "$pub_b64" | base64 -d >"$ADMIN_PUB_GENERIC"
chmod 0600 "$ADMIN_PRIV_GENERIC"
echo "Updating database superuser secret 'prole-db-superuser' from OpenBao ..."
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
# Mirror to legacy path for compatibility.
cp "$ADMIN_PRIV_GENERIC" "$ADMIN_PRIV_ED25519" 2>/dev/null || true
cp "$ADMIN_PUB_GENERIC" "$ADMIN_PUB_ED25519" 2>/dev/null || true
fi
local db_pass db_user
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
# 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
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
fi
@ -2783,7 +2769,7 @@ deploy_cluster() {
if [[ "$VERSION" == "latest" || -z "$VERSION" ]]; then
image=$(get_latest_image)
else
image="prole-db:$VERSION"
image="knoe-db:$VERSION"
fi
image=$(resolve_cnpg_image "$image")
sync_manifest_image "$image"

View File

@ -628,17 +628,23 @@ else
fi
if [[ "$ENABLE_KERBEROS" == "1" ]]; then
if [ -x "$SCRIPT_DIR/init_kdc.sh" ]; then
kdc_action="$ACTION"
case "$kdc_action" in
stop) kdc_action="cleanup" ;;
status) kdc_action="status" ;;
*) kdc_action="update" ;;
esac
SERVICE_NAMESPACE="$NS" PROLE_KDC_NAMESPACE="$NS" \
"$SCRIPT_DIR/init_kdc.sh" "$kdc_action" || rc=$?
# KDC is now embedded in the `prole-auth` pod (multi-container) by default.
# Only deploy a standalone KDC when explicitly requested.
if [[ "${PROLE_KDC_STANDALONE:-0}" == "1" ]]; then
if [ -x "$SCRIPT_DIR/init_kdc.sh" ]; then
kdc_action="$ACTION"
case "$kdc_action" in
stop) kdc_action="cleanup" ;;
status) kdc_action="status" ;;
*) 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
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

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_NAMESPACE=${K3S_REGISTRY_NAMESPACE:-${REGISTRY_NAMESPACE:-${SERVICE_NAMESPACE:-${PROLE_NAMESPACE:-default}}}}
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() {
if [[ "$(id -u)" -ne 0 ]]; then
@ -38,14 +48,28 @@ render_registries_yaml() {
local host="$1"
local port="$2"
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
mirrors:
"${host}:${port}":
endpoint:
- "https://${host}:${port}"
- "${scheme}://${host}:${port}"
"registry.${ns}.svc.cluster.local:${port}":
endpoint:
- "https://${host}:${port}"
- "${scheme}://${host}:${port}"
configs:
"${host}:${port}":
tls:
@ -64,8 +88,8 @@ case "$ACTION" in
fi
ensure_root
mkdir -p "$(dirname "$K3S_REGISTRY_FILE")"
render_registries_yaml "$K3S_REGISTRY_HOST" "$K3S_REGISTRY_PORT" "$K3S_REGISTRY_NAMESPACE" >"$K3S_REGISTRY_FILE"
echo "Wrote $K3S_REGISTRY_FILE for ${K3S_REGISTRY_HOST}:${K3S_REGISTRY_PORT}"
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_SCHEME}://${K3S_REGISTRY_HOST}:${K3S_REGISTRY_PORT}"
echo "Restart k3s to apply: sudo systemctl restart k3s"
;;
status)

View File

@ -31,6 +31,9 @@ prole_ensure_kube_context || exit 1
ACTION=${1:-initialize}
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_NAME=${PROLE_KDC_NAME:-auth}
PROLE_KDC_SERVICE=${PROLE_KDC_SERVICE:-auth}
@ -810,6 +813,112 @@ EOF
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"
if [[ "$deployment_present" -eq 0 ]]; then
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 deployment "$PROLE_KDC_NAME" --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() {

View File

@ -481,6 +481,11 @@ default_port_forward_if_local() {
}
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
return 0
fi

View File

@ -197,7 +197,7 @@ export_backup_bucket() {
local image pod_name export_subdir
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)"
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=$?
if [[ "$ENABLE_KERBEROS" == "1" ]]; then
SERVICE_NAMESPACE="$ns" PROLE_KDC_NAMESPACE="$ns" \
"$SCRIPT_DIR/init_kdc.sh" "$kdc_action" || rc=$?
# KDC is embedded in `prole-auth` by default. Only deploy standalone KDC when requested.
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
return "$rc"
@ -265,7 +270,7 @@ cleanup_old_namespace() {
"$SCRIPT_DIR/init_garage_store.sh" stop || true
KONG_NAMESPACE="$ns" SERVICE_NAMESPACE="$ns" \
"$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" \
"$SCRIPT_DIR/init_kdc.sh" cleanup || true
fi

View File

@ -721,7 +721,7 @@ root = sys.argv[1]
pw = sys.argv[2]
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))
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)
# 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
# shellcheck disable=SC1090
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"
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() {
local s="$1"
s="${s#"${s%%[![:space:]]*}"}"
@ -441,6 +460,17 @@ if [[ -n "$_prole_cfg_file" ]]; then
if [[ -n "$_cfg_sns" ]]; then
export SERVICE_NAMESPACE="$_cfg_sns"
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[@]}")
if [[ -z "$_cfg_sh" ]]; then
_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
export KUBECONTEXT="$_cfg_ctx"
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
if [[ -z "${PROLE_HOME:-}" && -d "$_prole_cfg_home_guess" ]]; then

View File

@ -6,8 +6,8 @@ set -euo pipefail
# Purpose:
# - Thin wrapper around the Python-native cluster repair logic.
#
# The real orchestration now lives in `installer/core/actions.py` and is
# invoked through `installer/core/repair_pipeline_cli.py`.
# The real orchestration now lives in `knoe/core/actions.py` and is
# invoked through `knoe/core/repair_pipeline_cli.py`.
SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
ROOT_DIR=$(cd "$SCRIPT_DIR/.." && pwd)
@ -88,4 +88,4 @@ if [[ -n "$MODE" ]]; then
fi
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]
sys.path.append(str(ROOT_DIR))
from installer import config as inst_config
from knoe import config as inst_config
def main():

View File

@ -12,7 +12,7 @@ import tkinter as tk # noqa: F401
from tkinter import ttk, scrolledtext, messagebox, filedialog # noqa: F401
from pathlib import Path # noqa: F401
from installer.ui.screens import (
from knoe.ui.screens import (
ProleInstaller,
ProleController,
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
spec:
instances: 3
imageName: myrddin.prole.org:5000/prole-db:0.0.0
imageName: k3d-prole-registry:5000/knoe-db:17-137
postgresUID: 100
postgresGID: 101
maxSyncReplicas: 1

View File

@ -4,7 +4,7 @@ metadata:
name: prole-db
spec:
instances: 3
imageName: myrddin.prole.org:5000/prole-db:18-125
imageName: myrddin.prole.org:5000/knoe-db:18-125
postgresUID: 100
postgresGID: 101
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 ----------------
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
from tkinter import ttk, scrolledtext

View File

@ -1,8 +1,8 @@
"""
Shared configuration and utility functions for the Prole installer (root-level).
This mirrors `prole.installer.config` but is located under the root `installer/`
package per the refactor request. UI code should import from `installer.config`.
This mirrors `prole.knoe.config` but is located under the root `installer/`
package per the refactor request. UI code should import from `knoe.config`.
"""
from __future__ import annotations
@ -30,7 +30,7 @@ OPENBAO_PREFIX = "${OPENBAO:"
OPENBAO_SUFFIX = "}"
PROLE_SECRET_VERSION = "v1"
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)
SECRET_KEY_SPECS = {
@ -452,6 +452,83 @@ def _collect_cfg_vars(cfg: any) -> dict:
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:
if not val or not isinstance(val, str):
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
# update the active environment base file without mutating other environments.
try:
from installer import prole_conf
from knoe import prole_conf
conf_dir = prole_conf.resolve_prole_conf_dir(PROJECT_ROOT)
cfg_path = prole_conf.entrypoint_path(conf_dir)

View File

@ -15,7 +15,7 @@ import threading
import uuid
from typing import Callable, Sequence
from installer.config import (
from knoe.config import (
_expand_path,
_collect_cfg_vars,
_expand_cfg_value,
@ -28,10 +28,10 @@ from installer.config import (
_encrypt_cfg_secret,
_merge_kubeconfig,
)
from installer import prole_conf as prole_conf_mgr
from installer.core.controller import ProleController
from installer.core.env import * # noqa: F401,F403
from installer.core.env import (
from knoe import prole_conf as prole_conf_mgr
from knoe.core.controller import ProleController
from knoe.core.env import * # noqa: F401,F403
from knoe.core.env import (
_parse_ollama_host,
_bool_str,
_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,
_verify_k3s_services_status,
)
from installer.core.milestones import (
from knoe.core.milestones import (
DependenciesMilestone,
NetworkScanMilestone,
EnvSetupMilestone,
@ -78,8 +78,9 @@ from installer.core.milestones import (
SupabaseImagePreloadMilestone,
SupabaseMilestone,
)
from installer.core.stream_exec import run_streaming_cmd
from installer.core.policy import (
from knoe.core.stream_exec import run_streaming_cmd
from knoe.core.build_context import copy_build_context_dir
from knoe.core.policy import (
POLICY_CFG_KEY,
OPTIONAL_WORKLOADS_MIN_READY_SCHEDULABLE_NODES,
evaluate_optional_workloads_allowed,
@ -773,13 +774,13 @@ class ProleInstaller:
try:
return os.getlogin()
except Exception:
return "prole-db"
return "knoe"
def _sanitize_namespace(self, name: str) -> str:
cleaned = re.sub(r"[^a-z0-9-]+", "-", (name or "").lower())
cleaned = re.sub(r"-{2,}", "-", cleaned).strip("-")
if not cleaned:
cleaned = "prole-db"
cleaned = "knoe-db"
if len(cleaned) > 63:
cleaned = cleaned[:63].rstrip("-")
return cleaned
@ -787,13 +788,13 @@ class ProleInstaller:
def _generate_namespace_name(self) -> str:
owner = self._sanitize_namespace(self._get_local_owner())
suffix = uuid.uuid4().hex[:6]
base = f"prole-db-{owner}-{suffix}"
base = f"knoe-db-{owner}-{suffix}"
return self._sanitize_namespace(base)
def _ensure_namespace_prefix(self, name: str) -> str:
cleaned = (name or "").strip()
if not cleaned:
return "prole-db"
return "knoe-db"
return cleaned
def _read_existing_cfg_namespace(self) -> str | None:
@ -1073,12 +1074,29 @@ class ProleInstaller:
cluster_env = self._get_input("init_cluster.cluster_env", "dev")
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":
home.mkdir(parents=True, exist_ok=True)
for key in ("PROLE_CONF", "PROLE_DATA", "PROLE_LOGS", "PROLE_SERVICE"):
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:
pass
@ -1149,8 +1167,21 @@ class ProleInstaller:
def _deploy_env_resources(self, values: dict):
"""Copy init scripts and etc directory into PROLE_HOME / PROLE_SERVICE."""
try:
prole_home = Path(values["PROLE_HOME"]).expanduser()
prole_service = Path(values["PROLE_SERVICE"]).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
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 = [
self.project_root / "src" / "prole" / "etc" / "init-port-forward.sh",
@ -1184,9 +1215,10 @@ class ProleInstaller:
pass
def reload_env_from_shell(self) -> None:
home = Path(
self._get_input("env_setup.PROLE_HOME", str(Path.home() / ".prole"))
)
raw_home = 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"
cmd = (
f"export PROLE_HOME={shlex.quote(str(home))}; "
@ -4271,7 +4303,7 @@ class ProleConsoleInstaller(ProleInstaller):
return
tag = self.controller.get_prole_db_version()
image_name = f"prole-db:{tag}"
image_name = f"knoe-db:{tag}"
env_key = _normalize_cluster_env(
self._get_input("init_cluster.cluster_env", "dev")
)
@ -4309,7 +4341,7 @@ class ProleConsoleInstaller(ProleInstaller):
if not registry or registry == "localhost:5000":
push_ok = False
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:
remote_tag = f"{registry}/{image_name}"
@ -4337,16 +4369,12 @@ class ProleConsoleInstaller(ProleInstaller):
)
return
self.log("==> Build prole-db image")
self.log("==> Build knoe-db image")
prole_home = Path.home() / ".prole"
build_dir = prole_home / "build" / "prole-db"
build_dir.mkdir(parents=True, exist_ok=True)
source_dir = get_resource_path("prole-db")
if source_dir.exists():
if source_dir.resolve() != build_dir.resolve():
if build_dir.exists():
shutil.rmtree(build_dir)
shutil.copytree(source_dir, build_dir)
copy_build_context_dir(source_dir, build_dir)
pub_key_path = Path.home() / ".ssh" / "id_prole_ed25519.pub"
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(
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
namespace = (
self._get_input("init_password.db_namespace", "") or ""
@ -5611,63 +5646,63 @@ def _prepare_k3s_pipeline(
_log("==> Loading installer config...\n")
installer = ProleConsoleInstaller(controller, str(cfg_path))
try:
existing_inputs = installer._load_inputs_from_cfg()
existing_inputs = knoe._load_inputs_from_cfg()
except Exception:
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"):
installer.inputs["init_cluster.k3s_server_url"] = k3s_server
if k3s_token and not installer.inputs.get("init_cluster.k3s_token"):
installer.inputs["init_cluster.k3s_token"] = k3s_token
if k3s_server and not knoe.inputs.get("init_cluster.k3s_server_url"):
knoe.inputs["init_cluster.k3s_server_url"] = k3s_server
if k3s_token and not knoe.inputs.get("init_cluster.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:
namespace = (
installer._get_input("env_setup.NAMESPACE", "") or ""
knoe._get_input("env_setup.NAMESPACE", "") or ""
).strip() or "default"
_log(f" Target namespace: {namespace}\n")
_log(" [OK] Config loaded\n\n")
if not skip_validation:
# Standalone mode: write a pipeline-specific cfg and validate
installer.inputs["init_cluster.cluster_env"] = "prole-service-cluster"
installer.inputs["init_cluster.supabase_enabled"] = _bool_str(False)
installer.inputs["init_cluster.kerberos_enabled"] = _bool_str(False)
installer.inputs["init_cluster.at_rest_encryption_enabled"] = _bool_str(True)
installer.inputs["kerberos_config.enabled"] = _bool_str(False)
installer.inputs["kerberos_config.test_connection"] = _bool_str(False)
knoe.inputs["init_cluster.cluster_env"] = "prole-service-cluster"
knoe.inputs["init_cluster.supabase_enabled"] = _bool_str(False)
knoe.inputs["init_cluster.kerberos_enabled"] = _bool_str(False)
knoe.inputs["init_cluster.at_rest_encryption_enabled"] = _bool_str(True)
knoe.inputs["kerberos_config.enabled"] = _bool_str(False)
knoe.inputs["kerberos_config.test_connection"] = _bool_str(False)
installer.inputs["env_setup.PROLE_HOME"] = str(project_root)
installer.inputs["env_setup.PROLE_CONF"] = str(project_root / "conf")
installer.inputs["env_setup.PROLE_DATA"] = installer._resolve_env_value(
knoe.inputs["env_setup.PROLE_HOME"] = str(project_root)
knoe.inputs["env_setup.PROLE_CONF"] = str(project_root / "conf")
knoe.inputs["env_setup.PROLE_DATA"] = knoe._resolve_env_value(
"PROLE_DATA", 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")
) 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
if not installer.inputs.get("env_setup.NAMESPACE"):
installer.inputs["env_setup.NAMESPACE"] = (
installer.inputs.get("init_password.db_namespace", "") or "default"
if not knoe.inputs.get("env_setup.NAMESPACE"):
knoe.inputs["env_setup.NAMESPACE"] = (
knoe.inputs.get("init_password.db_namespace", "") or "default"
)
if not installer.inputs.get("init_password.db_namespace"):
installer.inputs["init_password.db_namespace"] = (
installer.inputs.get("env_setup.NAMESPACE", "") or "default"
if not knoe.inputs.get("init_password.db_namespace"):
knoe.inputs["init_password.db_namespace"] = (
knoe.inputs.get("env_setup.NAMESPACE", "") or "default"
)
if not installer.inputs.get("kerberos_config.init_authority"):
installer.inputs["kerberos_config.init_authority"] = _bool_str(False)
if not installer.inputs.get("init_password.db_host_port"):
installer.inputs["init_password.db_host_port"] = "5432"
if not knoe.inputs.get("kerberos_config.init_authority"):
knoe.inputs["kerberos_config.init_authority"] = _bool_str(False)
if not knoe.inputs.get("init_password.db_host_port"):
knoe.inputs["init_password.db_host_port"] = "5432"
_log(" [OK] Config overrides applied for k3s pipeline\n")
_log("==> Writing prole.cfg...\n")
installer._write_cfg()
knoe._write_cfg()
_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")
attempt = 1
@ -5685,8 +5720,8 @@ def _prepare_k3s_pipeline(
break
_log(f" [WARN] Silent install test failed (code {rc})\n")
cur_server = installer.inputs.get("init_cluster.k3s_server_url", k3s_server)
cur_token = installer.inputs.get("init_cluster.k3s_token", k3s_token)
cur_server = knoe.inputs.get("init_cluster.k3s_server_url", k3s_server)
cur_token = knoe.inputs.get("init_cluster.k3s_token", k3s_token)
if attempt == 1:
_log(" Attempting k3s repair...\n")
@ -5706,8 +5741,8 @@ def _prepare_k3s_pipeline(
else:
_log("==> Skipping validation (silent install already succeeded)\n\n")
cur_server = installer.inputs.get("init_cluster.k3s_server_url", k3s_server)
cur_token = installer.inputs.get("init_cluster.k3s_token", k3s_token)
cur_server = knoe.inputs.get("init_cluster.k3s_server_url", k3s_server)
cur_token = knoe.inputs.get("init_cluster.k3s_token", k3s_token)
_sync_opentofu_pipeline(project_root, namespace, cur_server, cur_token, log_fn=_log)
_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 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:
from installer.milestone import Milestone
from knoe.milestone import Milestone
class ProleController:
@ -61,15 +62,26 @@ class ProleController:
if args is None:
args = []
base_env = os.environ.copy()
if env:
try:
base_env.update(env)
except Exception:
pass
# Get paths
source_script = self.project_root / "etc" / script_name
# 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:
prole_home = Path.home() / ".prole"
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.mkdir(parents=True, exist_ok=True)
@ -178,7 +190,24 @@ class ProleController:
self.logger.debug(f"Starting subprocess: {' '.join(cmd)}")
# 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(
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 installer import config as inst_config
from installer import prole_conf
from installer.core.policy import OPTIONAL_WORKLOADS_MIN_READY_SCHEDULABLE_NODES, POLICY_CFG_KEY
from knoe import config as inst_config
from knoe import prole_conf
from knoe.core.policy import OPTIONAL_WORKLOADS_MIN_READY_SCHEDULABLE_NODES, POLICY_CFG_KEY
from typing import Any
@ -84,7 +84,7 @@ OPENBAO_PREFIX = "${OPENBAO:"
OPENBAO_SUFFIX = "}"
PROLE_SECRET_VERSION = "v1"
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)
SECRET_KEY_SPECS = {

View File

@ -7,19 +7,19 @@ import time
from pathlib import Path
from typing import TYPE_CHECKING
from installer.milestone import Milestone
from installer import config as inst_config
from installer import prole_conf
from installer.core.stream_exec import run_streaming_cmd
from installer.core.policy import (
from knoe.milestone import Milestone
from knoe import config as inst_config
from knoe import prole_conf
from knoe.core.stream_exec import run_streaming_cmd
from knoe.core.policy import (
POLICY_CFG_KEY,
OPTIONAL_WORKLOADS_MIN_READY_SCHEDULABLE_NODES,
evaluate_optional_workloads_allowed,
)
if TYPE_CHECKING:
from installer.state import InstallerState
from installer.milestone import ProgressCallback
from knoe.state import InstallerState
from knoe.milestone import ProgressCallback
def _stream_line(line: str) -> None:
@ -376,10 +376,10 @@ class DockerBuildMilestone(Milestone):
return
# 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
# 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 = (
_normalize_cluster_env(state.inputs.get("init_cluster.cluster_env", "dev"))
@ -409,7 +409,7 @@ class DockerBuildMilestone(Milestone):
server = server.split("://")[1]
if ":" in server:
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)
if rc != 0:
@ -427,7 +427,7 @@ class ClusterLifecycleMilestone(Milestone):
def execute(
self, state: InstallerState, progress: ProgressCallback | 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")
env_key = _normalize_cluster_env(cluster_env) or cluster_env
@ -465,7 +465,7 @@ class ClusterLifecycleMilestone(Milestone):
)
res_stdout = res.stdout or ""
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(
"env_setup.PROLE_DATA",
@ -495,9 +495,9 @@ class ClusterLifecycleMilestone(Milestone):
except Exception:
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()
local_image = f"prole-db:{tag}"
local_image = f"knoe-db:{tag}"
if (
subprocess.run(
["docker", "image", "inspect", local_image], capture_output=True
@ -512,7 +512,7 @@ class ClusterLifecycleMilestone(Milestone):
registry = registry.replace(".localhost", "")
if not registry:
registry = "k3d-prole-registry:5000"
remote_tag = f"{registry}/prole-db:{tag}"
remote_tag = f"{registry}/knoe-db:{tag}"
subprocess.run(
["docker", "tag", local_image, remote_tag], capture_output=True
)
@ -932,7 +932,7 @@ class KerberosMilestone(Milestone):
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 = (
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)
return
from installer.core.env import _parse_bool
from knoe.core.env import _parse_bool
env = self._get_script_env(state)
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)
return
from installer.core.env import _parse_bool
from knoe.core.env import _parse_bool
env = self._get_script_env(state)
script_path = state.controller.project_root / "supabase" / "deploy.sh"

View File

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

View File

@ -15,7 +15,7 @@ import threading
from pathlib import Path
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:

View File

@ -5,9 +5,9 @@ import os
import sys
from pathlib import Path
from installer import prole_conf
from installer.core.controller import ProleController
from installer.core.actions import ProleConsoleInstaller
from knoe import prole_conf
from knoe.core.controller import ProleController
from knoe.core.actions import ProleConsoleInstaller
def _project_root() -> Path:
@ -84,42 +84,42 @@ def main(argv: list[str] | None = None) -> int:
installer = ProleConsoleInstaller(controller, cfg_path=cfg)
try:
existing_inputs = installer._load_inputs_from_cfg()
existing_inputs = knoe._load_inputs_from_cfg()
except Exception:
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.).
if mode == "k3s":
installer.inputs["init_cluster.cluster_env"] = "service"
knoe.inputs["init_cluster.cluster_env"] = "service"
elif mode == "k3d":
installer.inputs["init_cluster.cluster_env"] = "dev"
knoe.inputs["init_cluster.cluster_env"] = "dev"
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 = (
(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 "default"
)
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["DB_NAMESPACE"] = db_ns
env["NAMESPACE"] = service_ns
anomalies: list[str] = []
if installer._dashboard_kong_missing(env):
if knoe._dashboard_kong_missing(env):
anomalies.append("dashboard")
if installer._authority_context_missing():
if knoe._authority_context_missing():
anomalies.append("authority")
installer._repair_stale_released_pvs(env)
installer._run_cluster_repair_pipeline(env=env, anomalies=anomalies)
knoe._repair_stale_released_pvs(env)
knoe._run_cluster_repair_pipeline(env=env, anomalies=anomalies)
return 0

View File

@ -180,7 +180,7 @@ def _create_deploy_row(app, parent, step: dict):
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:
- Show completed steps from earlier phases

View File

@ -29,12 +29,12 @@ def clear_canvas_page(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)
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)

View File

@ -110,7 +110,7 @@ class Milestone(ABC):
service_ns = env["NAMESPACE"]
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")
mode = _deployment_mode_from_env(cluster_env) or "k3d"

View File

@ -970,7 +970,7 @@ def run_ncurses_installer(controller):
def _main(stdscr):
installer = ProleNcursesInstaller(stdscr, controller)
installer.run()
knoe.run()
try:
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"
# 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
if env == "test":
ns = "prole-test"
sns = "prole-test"
elif env == "dev":
ns = "prole-db"
ns = "knoe-db"
sns = "default"
else:
ns = "prole-db"
ns = "knoe-db"
sns = "knoe-system"
return (

View File

@ -1,6 +1,6 @@
"""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("__")})
__all__ = [name for name in globals() if not name.startswith("__")]

View File

@ -18,11 +18,11 @@ import tkinter as tk
import uuid
from tkinter import ttk, messagebox, filedialog
from installer import deploy as inst_deploy
from installer import prole_conf
from installer import screen as ui
from installer.build import get_build_command as inst_get_build_command
from installer.config import (
from knoe import deploy as inst_deploy
from knoe import prole_conf
from knoe import screen as ui
from knoe.build import get_build_command as inst_get_build_command
from knoe.config import (
_expand_cfg_value,
_collect_cfg_vars,
_is_prole_secret,
@ -32,8 +32,8 @@ from installer.config import (
_encrypt_cfg_secret,
_encrypt_prole_secret,
)
from installer.core.controller import ProleController
from installer.core.env import (
from knoe.core.controller import ProleController
from knoe.core.env import (
_detect_ansible_topology,
_format_ansible_topology_summary,
_normalize_k3s_token,
@ -68,8 +68,8 @@ if platform.system() == "Darwin":
except Exception:
pass
from installer.core.env import * # noqa: F401,F403
from installer.core.actions import (
from knoe.core.env import * # noqa: F401,F403
from knoe.core.actions import (
ProleInstaller,
ProleConsoleInstaller,
_prepare_k3s_pipeline,
@ -82,27 +82,27 @@ GLOBAL_SCAN_FRAMES: list = []
# ---------------------------------------------------------------------------
# Mixin imports — one per screen / concern
# ---------------------------------------------------------------------------
from installer.ui.screens.base import ScreenBaseMixin
from installer.ui.screens.navigation import NavigationMixin
from installer.ui.screens.welcome import WelcomeScreenMixin
from installer.ui.screens.dependencies import DependenciesScreenMixin
from installer.ui.screens.network import NetworkScreenMixin
from installer.ui.screens.environment import EnvironmentScreenMixin
from installer.ui.screens.database import DatabaseScreenMixin
from installer.ui.screens.database_options import DatabaseOptionsScreenMixin
from installer.ui.screens.cluster import ClusterScreenMixin
from installer.ui.screens.cluster_nodes import ClusterNodesScreenMixin
from installer.ui.screens.services import ServicesScreenMixin
from installer.ui.screens.security import SecurityScreenMixin
from installer.ui.screens.supabase import SupabaseScreenMixin
from installer.ui.screens.gitops import GitOpsScreenMixin
from installer.ui.screens.argocd import ArgoCDScreenMixin
from installer.ui.screens.docker import DockerScreenMixin
from installer.ui.screens.build import BuildScreenMixin
from installer.ui.screens.packaging import PackagingScreenMixin
from installer.ui.screens.deploy import DeployScreenMixin
from installer.ui.screens.validate import ValidateScreenMixin
from installer.ui.screens.cfg import ConfigMixin
from knoe.ui.screens.base import ScreenBaseMixin
from knoe.ui.screens.navigation import NavigationMixin
from knoe.ui.screens.welcome import WelcomeScreenMixin
from knoe.ui.screens.dependencies import DependenciesScreenMixin
from knoe.ui.screens.network import NetworkScreenMixin
from knoe.ui.screens.environment import EnvironmentScreenMixin
from knoe.ui.screens.database import DatabaseScreenMixin
from knoe.ui.screens.database_options import DatabaseOptionsScreenMixin
from knoe.ui.screens.cluster import ClusterScreenMixin
from knoe.ui.screens.cluster_nodes import ClusterNodesScreenMixin
from knoe.ui.screens.services import ServicesScreenMixin
from knoe.ui.screens.security import SecurityScreenMixin
from knoe.ui.screens.supabase import SupabaseScreenMixin
from knoe.ui.screens.gitops import GitOpsScreenMixin
from knoe.ui.screens.argocd import ArgoCDScreenMixin
from knoe.ui.screens.docker import DockerScreenMixin
from knoe.ui.screens.build import BuildScreenMixin
from knoe.ui.screens.packaging import PackagingScreenMixin
from knoe.ui.screens.deploy import DeployScreenMixin
from knoe.ui.screens.validate import ValidateScreenMixin
from knoe.ui.screens.cfg import ConfigMixin
# ---------------------------------------------------------------------------
@ -651,7 +651,7 @@ class ProleInstaller(
try:
self.db_username = tk.StringVar(value=os.getlogin())
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_confirm = tk.StringVar()
self.namespace_owner = self._get_local_owner()
@ -856,7 +856,7 @@ def main():
else:
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)
@ -867,7 +867,7 @@ def main():
)
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
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("Falling back to ncurses interface...", file=sys.stderr)
time.sleep(1)
from installer.ncurses_installer import run_ncurses_installer
from knoe.ncurses_installer import run_ncurses_installer
run_ncurses_installer(controller)
else:
_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)

View File

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

View File

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

View File

@ -11,11 +11,11 @@ import time
import webbrowser
import tkinter as tk
from tkinter import ttk, messagebox, filedialog
from installer.screen import TerminalConsole
from installer import deploy as inst_deploy
from installer.build import get_build_command as inst_get_build_command
from installer import screen as ui
from installer.core.env import PROJECT_ROOT
from knoe.screen import TerminalConsole
from knoe import deploy as inst_deploy
from knoe.build import get_build_command as inst_get_build_command
from knoe import screen as ui
from knoe.core.env import PROJECT_ROOT
class BuildScreenMixin:
@ -793,7 +793,7 @@ echo "-------------------------------------------------------------------";
pass
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)
# ---------------- Build Summary page ----------------

View File

@ -9,8 +9,8 @@ import subprocess
from pathlib import Path
import tkinter as tk
from tkinter import ttk, messagebox, filedialog
from installer import prole_conf
from installer.core.env import (
from knoe import prole_conf
from knoe.core.env import (
DEFAULT_ACTION_FLAGS,
DEFAULT_OLLAMA_PORT,
PROJECT_ROOT,
@ -28,7 +28,7 @@ from installer.core.env import (
_read_k3s_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:

View File

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

View File

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

View File

@ -3,15 +3,14 @@
import json
import os
import re
import shutil
import subprocess
import threading
from pathlib import Path
import tkinter as tk
from tkinter import ttk, messagebox, filedialog, simpledialog
from installer import screen as ui
from installer.config import get_docker_build_platform_args
from installer.core.env import (
from tkinter import ttk, messagebox, simpledialog
from knoe import screen as ui
from knoe.config import get_docker_build_platform_args
from knoe.core.env import (
PROJECT_ROOT,
_bool_str,
_deployment_mode_from_env,
@ -20,6 +19,7 @@ from installer.core.env import (
_push_docker_image,
get_resource_path,
)
from knoe.core.build_context import copy_build_context_dir
class DatabaseScreenMixin:
@ -396,7 +396,7 @@ class DatabaseScreenMixin:
if vals:
ns = vals[1]
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
if ns == self.db_namespace.get():
# Already selected, don't trigger re-refresh/save
@ -562,14 +562,14 @@ class DatabaseScreenMixin:
if ns == "supabase":
port = "5432"
else:
# For prole-db namespaces
# For knoe-db namespaces
if ns == current_ns:
if supabase_enabled:
port = "15432"
else:
port = db_host_port or "5432"
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
if supabase_enabled:
port = "15432"
@ -940,7 +940,7 @@ class DatabaseScreenMixin:
self._render_title("Build Database Image", y=150)
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)
@ -1414,7 +1414,7 @@ class DatabaseScreenMixin:
)
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
# This avoids issues with PyInstaller's temporary _MEIPASS directory
@ -1424,14 +1424,7 @@ class DatabaseScreenMixin:
# Copy prole-db directory to writable location
source_dir = get_resource_path("prole-db")
if source_dir.exists():
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)
copy_build_context_dir(source_dir, build_dir)
cwd = build_dir
@ -1498,7 +1491,7 @@ class DatabaseScreenMixin:
import_tag = image_name
if 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(
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")
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
if cluster_registry.endswith(".localhost:5000"):
alt_registry = cluster_registry.replace(
".localhost", ""
)
alt_tag = f"{alt_registry}/prole-db:{tag}"
alt_tag = f"{alt_registry}/knoe-db:{tag}"
if alt_tag != import_tag:
self._db_build_console.write(
f"Tagging image for k3d import: {alt_tag}\n"
@ -1647,7 +1640,7 @@ class DatabaseScreenMixin:
)
return
remote_tag = f"{registry_url}/prole-db:{tag}"
remote_tag = f"{registry_url}/knoe-db:{tag}"
self._db_build_console.write(
f"Tagging image for registry: {remote_tag}\n"
)

View File

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

View File

@ -5,8 +5,8 @@ import threading
import time
import tkinter as tk
from tkinter import ttk, messagebox, filedialog
from installer import config as inst_config
from installer import screen as ui
from knoe import config as inst_config
from knoe import screen as ui
class DependenciesScreenMixin:
@ -627,9 +627,9 @@ class DependenciesScreenMixin:
return True
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)
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)

View File

@ -8,8 +8,8 @@ import threading
from pathlib import Path
import tkinter as tk
from tkinter import ttk, messagebox, filedialog
from installer import screen as ui
from installer.core.env import PROJECT_ROOT
from knoe import screen as ui
from knoe.core.env import PROJECT_ROOT
class DeployScreenMixin:

View File

@ -9,8 +9,8 @@ import sys
import time
from pathlib import Path
from installer.config import get_docker_build_platform_args
from installer.core.env import (
from knoe.config import get_docker_build_platform_args
from knoe.core.env import (
PROJECT_ROOT,
_collect_images_from_files,
_deployment_mode_from_env,
@ -424,10 +424,10 @@ class DockerScreenMixin:
return None
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 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"
if hba_src.exists():
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"
else:
content = content.replace(
"krb_realm=EXAMPLE.COM", f"krb_realm={realm}"
"krb_realm=KNOEY.COM", f"krb_realm={realm}"
)
hba_src.write_text(content)
print(f"Updated {hba_src} with realm {realm}")
# Base local image tag (before pushing to registry)
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
conf_src = PROJECT_ROOT / "conf" / "postgresql"
@ -514,9 +514,9 @@ class DockerScreenMixin:
return
version = self.get_prole_db_version()
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)
self.remote_image_tag = f"{registry}/prole-db:{version}"
self.remote_image_tag = f"{registry}/knoe-db:{version}"
subprocess.run(
["docker", "tag", image, self.remote_image_tag],
check=True,
@ -537,7 +537,7 @@ class DockerScreenMixin:
def import_k3d_image(self, cluster_name="knoe-dev-cluster"):
"""Import image to k3d cluster (only for Dev)."""
version = self.get_prole_db_version()
image_name = f"prole-db:{version}"
image_name = f"knoe-db:{version}"
subprocess.run(
["k3d", "image", "import", image_name, "-c", cluster_name],
check=True,
@ -556,12 +556,12 @@ class DockerScreenMixin:
if not manifest_path.exists():
continue
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)
# Update Kerberos realm in manifest if enabled
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:
# Insert GSS rule after existing scram rules for service accounts
new_content = new_content.replace(
@ -571,7 +571,7 @@ class DockerScreenMixin:
)
else:
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:

View File

@ -7,8 +7,8 @@ import subprocess
from pathlib import Path
import tkinter as tk
from tkinter import ttk, messagebox, filedialog
from installer import screen as ui
from installer.core.env import PROJECT_ROOT, _expand_cfg_value
from knoe import screen as ui
from knoe.core.env import PROJECT_ROOT, _expand_cfg_value
class EnvironmentScreenMixin:

View File

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

View File

@ -5,8 +5,8 @@ import platform
from pathlib import Path
import tkinter as tk
from tkinter import ttk, messagebox, filedialog
from installer import config as inst_config
from installer.core.env import get_resource_path, _deployment_mode_from_env
from knoe import config as inst_config
from knoe.core.env import get_resource_path, _deployment_mode_from_env
class NavigationMixin:

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