diff --git a/Makefile b/Makefile index 5878cf5..3e10b0c 100644 --- a/Makefile +++ b/Makefile @@ -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" diff --git a/conf/dev/prole.cfg b/conf/dev/prole.cfg index b770d78..b566aab 100644 --- a/conf/dev/prole.cfg +++ b/conf/dev/prole.cfg @@ -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. diff --git a/conf/port-mapping.cfg b/conf/port-mapping.cfg index 986985d..e1d6644 100644 --- a/conf/port-mapping.cfg +++ b/conf/port-mapping.cfg @@ -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 diff --git a/conf/postgresql/.version b/conf/postgresql/.version index 25bf17f..8e2afd3 100644 --- a/conf/postgresql/.version +++ b/conf/postgresql/.version @@ -1 +1 @@ -18 \ No newline at end of file +17 \ No newline at end of file diff --git a/conf/prod/prole.cfg b/conf/prod/prole.cfg index 8fcce1d..7c1ec1b 100644 --- a/conf/prod/prole.cfg +++ b/conf/prod/prole.cfg @@ -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 diff --git a/conf/service/prole.cfg b/conf/service/prole.cfg index 4a3632a..a773f42 100644 --- a/conf/service/prole.cfg +++ b/conf/service/prole.cfg @@ -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)] diff --git a/deploy/opentofu/k3s/manifests/prole/kustomization.yaml b/deploy/opentofu/k3s/manifests/prole/kustomization.yaml index 18b53f8..3dd4bee 100644 --- a/deploy/opentofu/k3s/manifests/prole/kustomization.yaml +++ b/deploy/opentofu/k3s/manifests/prole/kustomization.yaml @@ -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 diff --git a/deploy/opentofu/k3s/manifests/prole/prole-auth-deployment.yaml b/deploy/opentofu/k3s/manifests/prole/prole-auth-deployment.yaml index e37ff31..16a4bdf 100644 --- a/deploy/opentofu/k3s/manifests/prole/prole-auth-deployment.yaml +++ b/deploy/opentofu/k3s/manifests/prole/prole-auth-deployment.yaml @@ -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: {} diff --git a/deploy/opentofu/k3s/manifests/prole/prole-auth-kerberos-configmap.yaml b/deploy/opentofu/k3s/manifests/prole/prole-auth-kerberos-configmap.yaml index 1352ba5..103b3f3 100644 --- a/deploy/opentofu/k3s/manifests/prole/prole-auth-kerberos-configmap.yaml +++ b/deploy/opentofu/k3s/manifests/prole/prole-auth-kerberos-configmap.yaml @@ -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" diff --git a/deploy/opentofu/k3s/manifests/prole/prole-auth-service.yaml b/deploy/opentofu/k3s/manifests/prole/prole-auth-service.yaml index 66f3bfb..5ce43a0 100644 --- a/deploy/opentofu/k3s/manifests/prole/prole-auth-service.yaml +++ b/deploy/opentofu/k3s/manifests/prole/prole-auth-service.yaml @@ -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 diff --git a/deploy/opentofu/k3s/manifests/prole/prole-db.yaml b/deploy/opentofu/k3s/manifests/prole/prole-db.yaml index 48d23de..571e888 100644 --- a/deploy/opentofu/k3s/manifests/prole/prole-db.yaml +++ b/deploy/opentofu/k3s/manifests/prole/prole-db.yaml @@ -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 diff --git a/deploy/opentofu/k3s/manifests/prole/prole-kdc-configmap.yaml b/deploy/opentofu/k3s/manifests/prole/prole-kdc-configmap.yaml new file mode 100644 index 0000000..46a1394 --- /dev/null +++ b/deploy/opentofu/k3s/manifests/prole/prole-kdc-configmap.yaml @@ -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 diff --git a/deploy/opentofu/k3s/manifests/prole/prole-kdc-secrets.example.yaml b/deploy/opentofu/k3s/manifests/prole/prole-kdc-secrets.example.yaml new file mode 100644 index 0000000..1777e22 --- /dev/null +++ b/deploy/opentofu/k3s/manifests/prole/prole-kdc-secrets.example.yaml @@ -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" diff --git a/docs/grafana-sso.md b/docs/grafana-sso.md index 507eddc..0e1c716 100644 --- a/docs/grafana-sso.md +++ b/docs/grafana-sso.md @@ -36,12 +36,14 @@ This repo’s 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 diff --git a/docs/ncurses-installer.md b/docs/ncurses-installer.md index 672f189..659e0ea 100644 --- a/docs/ncurses-installer.md +++ b/docs/ncurses-installer.md @@ -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 diff --git a/etc/build_db.sh b/etc/build_db.sh index c9d9b16..3016b94 100755 --- a/etc/build_db.sh +++ b/etc/build_db.sh @@ -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 diff --git a/etc/init_cloudnative_pg.sh b/etc/init_cloudnative_pg.sh index b0f433e..af419c2 100755 --- a/etc/init_cloudnative_pg.sh +++ b/etc/init_cloudnative_pg.sh @@ -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" diff --git a/etc/init_common_services.sh b/etc/init_common_services.sh index 29cec3c..9699055 100755 --- a/etc/init_common_services.sh +++ b/etc/init_common_services.sh @@ -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 diff --git a/etc/init_k3s_registry.sh b/etc/init_k3s_registry.sh index df64827..f4c5088 100644 --- a/etc/init_k3s_registry.sh +++ b/etc/init_k3s_registry.sh @@ -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 <"$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) diff --git a/etc/init_kdc.sh b/etc/init_kdc.sh index efee5ec..5ec6780 100755 --- a/etc/init_kdc.sh +++ b/etc/init_kdc.sh @@ -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 </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() { diff --git a/etc/init_kerberos.sh b/etc/init_kerberos.sh index 2354d70..0711a7b 100755 --- a/etc/init_kerberos.sh +++ b/etc/init_kerberos.sh @@ -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 diff --git a/etc/init_prole-db-reset.sh b/etc/init_prole-db-reset.sh index e60ee24..cd136d2 100755 --- a/etc/init_prole-db-reset.sh +++ b/etc/init_prole-db-reset.sh @@ -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)" diff --git a/etc/init_service_layer.sh b/etc/init_service_layer.sh index 7438c77..0392251 100755 --- a/etc/init_service_layer.sh +++ b/etc/init_service_layer.sh @@ -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 diff --git a/etc/prole-db-passwwd.sh b/etc/prole-db-passwwd.sh index 0ac13a7..2eee1af 100755 --- a/etc/prole-db-passwwd.sh +++ b/etc/prole-db-passwwd.sh @@ -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 diff --git a/etc/prole_cfg.sh b/etc/prole_cfg.sh index b0505d3..a07463d 100644 --- a/etc/prole_cfg.sh +++ b/etc/prole_cfg.sh @@ -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 diff --git a/etc/repair_pipeline.sh b/etc/repair_pipeline.sh index e904f24..686dfbd 100755 --- a/etc/repair_pipeline.sh +++ b/etc/repair_pipeline.sh @@ -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[@]}" diff --git a/etc/sync-prole-cfg.py b/etc/sync-prole-cfg.py index f84cf5d..2e951c5 100644 --- a/etc/sync-prole-cfg.py +++ b/etc/sync-prole-cfg.py @@ -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(): diff --git a/install.py b/install.py index 35c63da..0a871d9 100755 --- a/install.py +++ b/install.py @@ -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, diff --git a/k3s/registries.yaml b/k3s/registries.yaml new file mode 100644 index 0000000..96f0362 --- /dev/null +++ b/k3s/registries.yaml @@ -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" diff --git a/k8s/prole/prole-db-recovery.yaml.tpl b/k8s/prole/prole-db-recovery.yaml.tpl index 0225f0a..52e66fb 100644 --- a/k8s/prole/prole-db-recovery.yaml.tpl +++ b/k8s/prole/prole-db-recovery.yaml.tpl @@ -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 diff --git a/k8s/prole/prole-db.yaml b/k8s/prole/prole-db.yaml index 1ac2476..2e7a718 100644 --- a/k8s/prole/prole-db.yaml +++ b/k8s/prole/prole-db.yaml @@ -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 diff --git a/knoe.spec b/knoe.spec new file mode 100644 index 0000000..8b170da --- /dev/null +++ b/knoe.spec @@ -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', + }, +) diff --git a/installer/README.md b/knoe/README.md similarity index 100% rename from installer/README.md rename to knoe/README.md diff --git a/installer/__init__.py b/knoe/__init__.py similarity index 100% rename from installer/__init__.py rename to knoe/__init__.py diff --git a/installer/build.py b/knoe/build.py similarity index 97% rename from installer/build.py rename to knoe/build.py index e811d80..17a4c32 100644 --- a/installer/build.py +++ b/knoe/build.py @@ -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 diff --git a/installer/config.py b/knoe/config.py similarity index 91% rename from installer/config.py rename to knoe/config.py index cc681f0..b7c9317 100644 --- a/installer/config.py +++ b/knoe/config.py @@ -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) diff --git a/installer/core.py b/knoe/core.py similarity index 100% rename from installer/core.py rename to knoe/core.py diff --git a/installer/core/__init__.py b/knoe/core/__init__.py similarity index 100% rename from installer/core/__init__.py rename to knoe/core/__init__.py diff --git a/installer/core/actions.py b/knoe/core/actions.py similarity index 97% rename from installer/core/actions.py rename to knoe/core/actions.py index 60b58ad..ef0edcf 100644 --- a/installer/core/actions.py +++ b/knoe/core/actions.py @@ -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") diff --git a/knoe/core/build_context.py b/knoe/core/build_context.py new file mode 100644 index 0000000..e2cb2fc --- /dev/null +++ b/knoe/core/build_context.py @@ -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, + ) diff --git a/installer/core/controller.py b/knoe/core/controller.py similarity index 88% rename from installer/core/controller.py rename to knoe/core/controller.py index a630261..73e135a 100644 --- a/installer/core/controller.py +++ b/knoe/core/controller.py @@ -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','')} NAMESPACE={run_env.get('NAMESPACE','')} PROLE_MODE={run_env.get('PROLE_MODE','')}" ) diff --git a/installer/core/env.py b/knoe/core/env.py similarity index 99% rename from installer/core/env.py rename to knoe/core/env.py index 5a36ee7..c22d84b 100644 --- a/installer/core/env.py +++ b/knoe/core/env.py @@ -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 = { diff --git a/installer/core/milestones.py b/knoe/core/milestones.py similarity index 97% rename from installer/core/milestones.py rename to knoe/core/milestones.py index 6dc5ded..e3cb537 100644 --- a/installer/core/milestones.py +++ b/knoe/core/milestones.py @@ -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" diff --git a/installer/core/monitor.py b/knoe/core/monitor.py similarity index 98% rename from installer/core/monitor.py rename to knoe/core/monitor.py index 958cdda..39a9d17 100644 --- a/installer/core/monitor.py +++ b/knoe/core/monitor.py @@ -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, diff --git a/installer/core/ollama_scan.py b/knoe/core/ollama_scan.py similarity index 99% rename from installer/core/ollama_scan.py rename to knoe/core/ollama_scan.py index 74714df..1d9d487 100644 --- a/installer/core/ollama_scan.py +++ b/knoe/core/ollama_scan.py @@ -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: diff --git a/installer/core/policy.py b/knoe/core/policy.py similarity index 100% rename from installer/core/policy.py rename to knoe/core/policy.py diff --git a/installer/core/repair_pipeline_cli.py b/knoe/core/repair_pipeline_cli.py similarity index 74% rename from installer/core/repair_pipeline_cli.py rename to knoe/core/repair_pipeline_cli.py index fcb1f15..662558d 100644 --- a/installer/core/repair_pipeline_cli.py +++ b/knoe/core/repair_pipeline_cli.py @@ -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 diff --git a/installer/core/stream_exec.py b/knoe/core/stream_exec.py similarity index 100% rename from installer/core/stream_exec.py rename to knoe/core/stream_exec.py diff --git a/installer/deploy.py b/knoe/deploy.py similarity index 99% rename from installer/deploy.py rename to knoe/deploy.py index ea973cf..b067e23 100644 --- a/installer/deploy.py +++ b/knoe/deploy.py @@ -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 diff --git a/installer/main.py b/knoe/main.py similarity index 89% rename from installer/main.py rename to knoe/main.py index 0289866..279a3ea 100644 --- a/installer/main.py +++ b/knoe/main.py @@ -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) diff --git a/installer/milestone.py b/knoe/milestone.py similarity index 99% rename from installer/milestone.py rename to knoe/milestone.py index ee93a16..7c818c8 100644 --- a/installer/milestone.py +++ b/knoe/milestone.py @@ -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" diff --git a/installer/ncurses_installer.py b/knoe/ncurses_installer.py similarity index 99% rename from installer/ncurses_installer.py rename to knoe/ncurses_installer.py index 7810dfa..369d289 100644 --- a/installer/ncurses_installer.py +++ b/knoe/ncurses_installer.py @@ -970,7 +970,7 @@ def run_ncurses_installer(controller): def _main(stdscr): installer = ProleNcursesInstaller(stdscr, controller) - installer.run() + knoe.run() try: curses.wrapper(_main) diff --git a/installer/ncurses_ui.py b/knoe/ncurses_ui.py similarity index 100% rename from installer/ncurses_ui.py rename to knoe/ncurses_ui.py diff --git a/knoe/ops/__init__.py b/knoe/ops/__init__.py new file mode 100644 index 0000000..baff840 --- /dev/null +++ b/knoe/ops/__init__.py @@ -0,0 +1,7 @@ +from __future__ import annotations + +from .context import KnoeContext + +__all__ = [ + "KnoeContext", +] diff --git a/knoe/ops/cli.py b/knoe/ops/cli.py new file mode 100644 index 0000000..a45d945 --- /dev/null +++ b/knoe/ops/cli.py @@ -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: + # /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()) diff --git a/knoe/ops/components/__init__.py b/knoe/ops/components/__init__.py new file mode 100644 index 0000000..737bf0e --- /dev/null +++ b/knoe/ops/components/__init__.py @@ -0,0 +1,10 @@ +from __future__ import annotations + +__all__ = [ + "argocd", + "common_core", + "garage_store", + "openbao", + "opentofu", + "registry", +] diff --git a/knoe/ops/components/argocd.py b/knoe/ops/components/argocd.py new file mode 100644 index 0000000..7fce85d --- /dev/null +++ b/knoe/ops/components/argocd.py @@ -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") diff --git a/knoe/ops/components/common_core.py b/knoe/ops/components/common_core.py new file mode 100644 index 0000000..768ef07 --- /dev/null +++ b/knoe/ops/components/common_core.py @@ -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") diff --git a/knoe/ops/components/garage_store.py b/knoe/ops/components/garage_store.py new file mode 100644 index 0000000..03a4548 --- /dev/null +++ b/knoe/ops/components/garage_store.py @@ -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") diff --git a/knoe/ops/components/openbao.py b/knoe/ops/components/openbao.py new file mode 100644 index 0000000..b962d9f --- /dev/null +++ b/knoe/ops/components/openbao.py @@ -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") diff --git a/knoe/ops/components/opentofu.py b/knoe/ops/components/opentofu.py new file mode 100644 index 0000000..a2faf6a --- /dev/null +++ b/knoe/ops/components/opentofu.py @@ -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") diff --git a/knoe/ops/components/registry.py b/knoe/ops/components/registry.py new file mode 100644 index 0000000..168c44a --- /dev/null +++ b/knoe/ops/components/registry.py @@ -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") diff --git a/knoe/ops/context.py b/knoe/ops/context.py new file mode 100644 index 0000000..9462720 --- /dev/null +++ b/knoe/ops/context.py @@ -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 diff --git a/knoe/ops/files.py b/knoe/ops/files.py new file mode 100644 index 0000000..f180eaa --- /dev/null +++ b/knoe/ops/files.py @@ -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 diff --git a/knoe/ops/helm.py b/knoe/ops/helm.py new file mode 100644 index 0000000..9cf183a --- /dev/null +++ b/knoe/ops/helm.py @@ -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)) diff --git a/knoe/ops/k8s.py b/knoe/ops/k8s.py new file mode 100644 index 0000000..c3e5ec5 --- /dev/null +++ b/knoe/ops/k8s.py @@ -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, + ) diff --git a/knoe/ops/legacy_shell.py b/knoe/ops/legacy_shell.py new file mode 100644 index 0000000..90b0ba9 --- /dev/null +++ b/knoe/ops/legacy_shell.py @@ -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 diff --git a/knoe/ops/process.py b/knoe/ops/process.py new file mode 100644 index 0000000..94bf850 --- /dev/null +++ b/knoe/ops/process.py @@ -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 diff --git a/knoe/ops/secrets.py b/knoe/ops/secrets.py new file mode 100644 index 0000000..837b997 --- /dev/null +++ b/knoe/ops/secrets.py @@ -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) diff --git a/knoe/ops/status.py b/knoe/ops/status.py new file mode 100644 index 0000000..5e98510 --- /dev/null +++ b/knoe/ops/status.py @@ -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 diff --git a/installer/prole_conf.py b/knoe/prole_conf.py similarity index 99% rename from installer/prole_conf.py rename to knoe/prole_conf.py index a60df45..1a89718 100644 --- a/installer/prole_conf.py +++ b/knoe/prole_conf.py @@ -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 ( diff --git a/installer/runner.py b/knoe/runner.py similarity index 100% rename from installer/runner.py rename to knoe/runner.py diff --git a/installer/screen.py b/knoe/screen.py similarity index 100% rename from installer/screen.py rename to knoe/screen.py diff --git a/installer/scripts/install_prole_cmctl.sh b/knoe/scripts/install_prole_cmctl.sh similarity index 100% rename from installer/scripts/install_prole_cmctl.sh rename to knoe/scripts/install_prole_cmctl.sh diff --git a/installer/scripts/install_prole_helm.sh b/knoe/scripts/install_prole_helm.sh similarity index 100% rename from installer/scripts/install_prole_helm.sh rename to knoe/scripts/install_prole_helm.sh diff --git a/installer/scripts/install_prole_homebrew.sh b/knoe/scripts/install_prole_homebrew.sh similarity index 100% rename from installer/scripts/install_prole_homebrew.sh rename to knoe/scripts/install_prole_homebrew.sh diff --git a/installer/scripts/install_prole_k3d.sh b/knoe/scripts/install_prole_k3d.sh similarity index 100% rename from installer/scripts/install_prole_k3d.sh rename to knoe/scripts/install_prole_k3d.sh diff --git a/installer/scripts/install_prole_krew.sh b/knoe/scripts/install_prole_krew.sh similarity index 100% rename from installer/scripts/install_prole_krew.sh rename to knoe/scripts/install_prole_krew.sh diff --git a/installer/scripts/install_prole_kubectl.sh b/knoe/scripts/install_prole_kubectl.sh similarity index 100% rename from installer/scripts/install_prole_kubectl.sh rename to knoe/scripts/install_prole_kubectl.sh diff --git a/installer/scripts/install_prole_kubectl_plugins.sh b/knoe/scripts/install_prole_kubectl_plugins.sh similarity index 100% rename from installer/scripts/install_prole_kubectl_plugins.sh rename to knoe/scripts/install_prole_kubectl_plugins.sh diff --git a/installer/scripts/install_prole_opentofu.sh b/knoe/scripts/install_prole_opentofu.sh similarity index 100% rename from installer/scripts/install_prole_opentofu.sh rename to knoe/scripts/install_prole_opentofu.sh diff --git a/installer/state.py b/knoe/state.py similarity index 100% rename from installer/state.py rename to knoe/state.py diff --git a/installer/ui/__init__.py b/knoe/ui/__init__.py similarity index 100% rename from installer/ui/__init__.py rename to knoe/ui/__init__.py diff --git a/installer/ui/legacy_tk.py b/knoe/ui/legacy_tk.py similarity index 82% rename from installer/ui/legacy_tk.py rename to knoe/ui/legacy_tk.py index 7db38b9..a2a88ed 100644 --- a/installer/ui/legacy_tk.py +++ b/knoe/ui/legacy_tk.py @@ -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("__")] diff --git a/installer/ui/ncurses_ui.py b/knoe/ui/ncurses_ui.py similarity index 100% rename from installer/ui/ncurses_ui.py rename to knoe/ui/ncurses_ui.py diff --git a/installer/ui/screens/__init__.py b/knoe/ui/screens/__init__.py similarity index 94% rename from installer/ui/screens/__init__.py rename to knoe/ui/screens/__init__.py index 5fa7517..b584457 100644 --- a/installer/ui/screens/__init__.py +++ b/knoe/ui/screens/__init__.py @@ -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) diff --git a/installer/ui/screens/argocd.py b/knoe/ui/screens/argocd.py similarity index 98% rename from installer/ui/screens/argocd.py rename to knoe/ui/screens/argocd.py index a62a7fe..9064af8 100644 --- a/installer/ui/screens/argocd.py +++ b/knoe/ui/screens/argocd.py @@ -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) diff --git a/installer/ui/screens/base.py b/knoe/ui/screens/base.py similarity index 99% rename from installer/ui/screens/base.py rename to knoe/ui/screens/base.py index 5e6ed7a..1d9b53b 100644 --- a/installer/ui/screens/base.py +++ b/knoe/ui/screens/base.py @@ -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: diff --git a/installer/ui/screens/build.py b/knoe/ui/screens/build.py similarity index 99% rename from installer/ui/screens/build.py rename to knoe/ui/screens/build.py index 9add0dd..7a9ca3d 100644 --- a/installer/ui/screens/build.py +++ b/knoe/ui/screens/build.py @@ -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 ---------------- diff --git a/installer/ui/screens/cfg.py b/knoe/ui/screens/cfg.py similarity index 99% rename from installer/ui/screens/cfg.py rename to knoe/ui/screens/cfg.py index 0eaea24..5063b70 100644 --- a/installer/ui/screens/cfg.py +++ b/knoe/ui/screens/cfg.py @@ -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: diff --git a/installer/ui/screens/cluster.py b/knoe/ui/screens/cluster.py similarity index 99% rename from installer/ui/screens/cluster.py rename to knoe/ui/screens/cluster.py index dd47e07..b67efb4 100644 --- a/installer/ui/screens/cluster.py +++ b/knoe/ui/screens/cluster.py @@ -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() diff --git a/installer/ui/screens/cluster_nodes.py b/knoe/ui/screens/cluster_nodes.py similarity index 99% rename from installer/ui/screens/cluster_nodes.py rename to knoe/ui/screens/cluster_nodes.py index c9c3183..5aa2e44 100644 --- a/installer/ui/screens/cluster_nodes.py +++ b/knoe/ui/screens/cluster_nodes.py @@ -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: diff --git a/installer/ui/screens/database.py b/knoe/ui/screens/database.py similarity index 98% rename from installer/ui/screens/database.py rename to knoe/ui/screens/database.py index 7f6d35b..7829077 100644 --- a/installer/ui/screens/database.py +++ b/knoe/ui/screens/database.py @@ -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" ) diff --git a/installer/ui/screens/database_options.py b/knoe/ui/screens/database_options.py similarity index 99% rename from installer/ui/screens/database_options.py rename to knoe/ui/screens/database_options.py index 6fe935d..2051e5c 100644 --- a/installer/ui/screens/database_options.py +++ b/knoe/ui/screens/database_options.py @@ -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.""" diff --git a/installer/ui/screens/dependencies.py b/knoe/ui/screens/dependencies.py similarity index 99% rename from installer/ui/screens/dependencies.py rename to knoe/ui/screens/dependencies.py index e6b7d31..ab5b109 100644 --- a/installer/ui/screens/dependencies.py +++ b/knoe/ui/screens/dependencies.py @@ -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) diff --git a/installer/ui/screens/deploy.py b/knoe/ui/screens/deploy.py similarity index 99% rename from installer/ui/screens/deploy.py rename to knoe/ui/screens/deploy.py index 052dd36..1e26306 100644 --- a/installer/ui/screens/deploy.py +++ b/knoe/ui/screens/deploy.py @@ -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: diff --git a/installer/ui/screens/docker.py b/knoe/ui/screens/docker.py similarity index 97% rename from installer/ui/screens/docker.py rename to knoe/ui/screens/docker.py index 4d5b1a1..29d4b6a 100644 --- a/installer/ui/screens/docker.py +++ b/knoe/ui/screens/docker.py @@ -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: diff --git a/installer/ui/screens/environment.py b/knoe/ui/screens/environment.py similarity index 99% rename from installer/ui/screens/environment.py rename to knoe/ui/screens/environment.py index 05072b6..6d8fac3 100644 --- a/installer/ui/screens/environment.py +++ b/knoe/ui/screens/environment.py @@ -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: diff --git a/installer/ui/screens/gitops.py b/knoe/ui/screens/gitops.py similarity index 98% rename from installer/ui/screens/gitops.py rename to knoe/ui/screens/gitops.py index bdb37a4..99fdfce 100644 --- a/installer/ui/screens/gitops.py +++ b/knoe/ui/screens/gitops.py @@ -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: diff --git a/installer/ui/screens/navigation.py b/knoe/ui/screens/navigation.py similarity index 99% rename from installer/ui/screens/navigation.py rename to knoe/ui/screens/navigation.py index 52a5713..e7c665d 100644 --- a/installer/ui/screens/navigation.py +++ b/knoe/ui/screens/navigation.py @@ -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: diff --git a/installer/ui/screens/network.py b/knoe/ui/screens/network.py similarity index 99% rename from installer/ui/screens/network.py rename to knoe/ui/screens/network.py index 6ccd74b..19d4d33 100644 --- a/installer/ui/screens/network.py +++ b/knoe/ui/screens/network.py @@ -16,15 +16,15 @@ from pathlib import Path import tkinter as tk from tkinter import ttk -from installer import screen as ui -from installer.core.env import DEFAULT_OLLAMA_PORT, get_resource_path, _format_ollama_host -from installer.core.ollama_scan import run_ollama_scan -from installer.core.stream_exec import run_streaming_cmd +from knoe import screen as ui +from knoe.core.env import DEFAULT_OLLAMA_PORT, get_resource_path, _format_ollama_host +from knoe.core.ollama_scan import run_ollama_scan +from knoe.core.stream_exec import run_streaming_cmd def _get_global_scan_frames() -> list: """Lazy accessor to avoid circular import with __init__.py.""" - import installer.ui.screens as _pkg + import knoe.ui.screens as _pkg return _pkg.GLOBAL_SCAN_FRAMES diff --git a/installer/ui/screens/ollama.py b/knoe/ui/screens/ollama.py similarity index 99% rename from installer/ui/screens/ollama.py rename to knoe/ui/screens/ollama.py index d6b32bc..ff14ee6 100644 --- a/installer/ui/screens/ollama.py +++ b/knoe/ui/screens/ollama.py @@ -6,15 +6,15 @@ import threading from pathlib import Path import tkinter as tk from tkinter import ttk -from installer import screen as ui -from installer.core.ollama_scan import run_ollama_scan -from installer.core.env import ( +from knoe import screen as ui +from knoe.core.ollama_scan import run_ollama_scan +from knoe.core.env import ( DEFAULT_OLLAMA_PORT, PROJECT_ROOT, _format_ollama_host, _parse_ollama_host, ) -from installer.config import _collect_cfg_vars, _expand_cfg_value +from knoe.config import _collect_cfg_vars, _expand_cfg_value class OllamaScreenMixin: diff --git a/installer/ui/screens/packaging.py b/knoe/ui/screens/packaging.py similarity index 99% rename from installer/ui/screens/packaging.py rename to knoe/ui/screens/packaging.py index e9cbe95..fa82d43 100644 --- a/installer/ui/screens/packaging.py +++ b/knoe/ui/screens/packaging.py @@ -12,10 +12,10 @@ import webbrowser 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 config as inst_config -from installer.core.env import PROJECT_ROOT, get_resource_path +from knoe.screen import TerminalConsole +from knoe import screen as ui +from knoe import config as inst_config +from knoe.core.env import PROJECT_ROOT, get_resource_path class PackagingScreenMixin: @@ -932,9 +932,9 @@ exec "$DIR/ProleTools.bin" "$@" threading.Thread(target=check, daemon=True).start() def generate_installer_script(self): - """Generate installer script by composing per-dependency templates from installer/scripts.""" + """Generate installer script by composing per-dependency templates from knoe/scripts.""" script_path = PROJECT_ROOT / "install_dependencies.sh" - scripts_dir = PROJECT_ROOT / "installer" / "scripts" + scripts_dir = PROJECT_ROOT / "knoe" / "scripts" # Expected order of templates expected = [ diff --git a/installer/ui/screens/security.py b/knoe/ui/screens/security.py similarity index 99% rename from installer/ui/screens/security.py rename to knoe/ui/screens/security.py index 96a500f..82c4f1b 100644 --- a/installer/ui/screens/security.py +++ b/knoe/ui/screens/security.py @@ -9,14 +9,14 @@ 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 ( +from knoe import screen as ui +from knoe.core.env import ( PROJECT_ROOT, _bool_str, _deployment_target_label, _ensure_ansible_vault_credentials, ) -from installer.config import ( +from knoe.config import ( SECRET_KEY_SPECS, _decrypt_prole_secret, _is_openbao_ref, diff --git a/installer/ui/screens/services.py b/knoe/ui/screens/services.py similarity index 99% rename from installer/ui/screens/services.py rename to knoe/ui/screens/services.py index 845d288..1c68985 100644 --- a/installer/ui/screens/services.py +++ b/knoe/ui/screens/services.py @@ -12,16 +12,16 @@ from pathlib import Path 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 installer.core.env import ( +from knoe.screen import TerminalConsole +from knoe import screen as ui +from knoe.core.env import ( PROJECT_ROOT, _bool_str, _deployment_mode_from_env, _deployment_target_label, _normalize_cluster_env, ) -from installer.core.policy import ( +from knoe.core.policy import ( POLICY_CFG_KEY, OPTIONAL_WORKLOADS_MIN_READY_SCHEDULABLE_NODES, evaluate_optional_workloads_allowed, diff --git a/installer/ui/screens/supabase.py b/knoe/ui/screens/supabase.py similarity index 98% rename from installer/ui/screens/supabase.py rename to knoe/ui/screens/supabase.py index 0a344a4..a3e81a4 100644 --- a/installer/ui/screens/supabase.py +++ b/knoe/ui/screens/supabase.py @@ -4,9 +4,9 @@ import os 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 SupabaseScreenMixin: diff --git a/installer/ui/screens/validate.py b/knoe/ui/screens/validate.py similarity index 98% rename from installer/ui/screens/validate.py rename to knoe/ui/screens/validate.py index 71adf0f..f9201e1 100644 --- a/installer/ui/screens/validate.py +++ b/knoe/ui/screens/validate.py @@ -6,8 +6,8 @@ import time import webbrowser 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 ValidateScreenMixin: diff --git a/installer/ui/screens/welcome.py b/knoe/ui/screens/welcome.py similarity index 99% rename from installer/ui/screens/welcome.py rename to knoe/ui/screens/welcome.py index f8af77a..bf37ad4 100644 --- a/installer/ui/screens/welcome.py +++ b/knoe/ui/screens/welcome.py @@ -4,7 +4,7 @@ import threading import time import tkinter as tk from tkinter import ttk, messagebox, filedialog -from installer import screen as ui +from knoe import screen as ui class WelcomeScreenMixin: diff --git a/installer/ui/tkinter_ui.py b/knoe/ui/tkinter_ui.py similarity index 100% rename from installer/ui/tkinter_ui.py rename to knoe/ui/tkinter_ui.py diff --git a/installer/welcome.txt b/knoe/welcome.txt similarity index 100% rename from installer/welcome.txt rename to knoe/welcome.txt diff --git a/mock_val/build_db.sh b/mock_val/build_db.sh index c9d9b16..3016b94 100755 --- a/mock_val/build_db.sh +++ b/mock_val/build_db.sh @@ -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 diff --git a/mock_val/init_cloudnative_pg.sh b/mock_val/init_cloudnative_pg.sh index b0ca6c0..d3bcb2f 100755 --- a/mock_val/init_cloudnative_pg.sh +++ b/mock_val/init_cloudnative_pg.sh @@ -487,7 +487,7 @@ get_latest_image() { release=$(printf "%03d" "$release") fi - echo "prole-db:${pg_version}-${release}" + echo "knoe-db:${pg_version}-${release}" } resolve_cnpg_image() { @@ -566,7 +566,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 @@ -1194,7 +1194,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}" @@ -1231,12 +1231,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 ..." @@ -1322,7 +1322,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 @@ -1330,7 +1330,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 @@ -1369,7 +1369,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") @@ -2143,57 +2143,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 @@ -2468,7 +2454,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" diff --git a/mock_val/init_prole-db-reset.sh b/mock_val/init_prole-db-reset.sh index e60ee24..cd136d2 100755 --- a/mock_val/init_prole-db-reset.sh +++ b/mock_val/init_prole-db-reset.sh @@ -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)" diff --git a/mock_val/prole-db-passwwd.sh b/mock_val/prole-db-passwwd.sh index 0ac13a7..2eee1af 100755 --- a/mock_val/prole-db-passwwd.sh +++ b/mock_val/prole-db-passwwd.sh @@ -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 diff --git a/mock_val/sync-prole-cfg.py b/mock_val/sync-prole-cfg.py index f84cf5d..2e951c5 100644 --- a/mock_val/sync-prole-cfg.py +++ b/mock_val/sync-prole-cfg.py @@ -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(): diff --git a/prole-db/.version b/prole-db/.version index 8306ec1..0973804 100644 --- a/prole-db/.version +++ b/prole-db/.version @@ -1 +1 @@ -130 \ No newline at end of file +137 \ No newline at end of file diff --git a/prole-db/Dockerfile b/prole-db/Dockerfile index e0e25b3..d8fe4af 100644 --- a/prole-db/Dockerfile +++ b/prole-db/Dockerfile @@ -39,15 +39,15 @@ RUN set -eux; \ dpkg -i "percona-release_latest.$(lsb_release -sc)_all.deb"; \ rm -f "percona-release_latest.$(lsb_release -sc)_all.deb"; \ apt-get update; \ - percona-release setup ppg-18; \ + percona-release setup ppg-17; \ apt-get update; \ apt-get install -y --no-install-recommends \ - percona-postgresql-18 \ - percona-postgresql-server-dev-18 \ + percona-postgresql-17 \ + percona-postgresql-server-dev-17 \ ; \ rm -rf /var/lib/apt/lists/* -ENV PATH=/usr/lib/postgresql/18/bin:$PATH +ENV PATH=/usr/lib/postgresql/17/bin:$PATH RUN set -eux; \ groupadd -r postgres --gid=999 || true; \ @@ -74,13 +74,13 @@ RUN set -eux; \ # Extension installation RUN set -eux; \ apt-get update; \ - apt-get install -y --no-install-recommends percona-postgresql-18-postgis-3; \ - apt-get install -y --no-install-recommends percona-postgresql-18-pgvector; \ - apt-get install -y --no-install-recommends percona-postgresql-18-pgaudit; \ - apt-get install -y --no-install-recommends percona-postgresql-18-repack; \ - apt-get install -y --no-install-recommends percona-pg-stat-monitor18; \ + apt-get install -y --no-install-recommends percona-postgresql-17-postgis-3; \ + apt-get install -y --no-install-recommends percona-postgresql-17-pgvector; \ + apt-get install -y --no-install-recommends percona-postgresql-17-pgaudit; \ + apt-get install -y --no-install-recommends percona-postgresql-17-repack; \ + apt-get install -y --no-install-recommends percona-pg-stat-monitor17; \ apt-get install -y --no-install-recommends percona-pgbadger; \ - apt-get install -y --no-install-recommends percona-pg-tde18; \ + apt-get install -y --no-install-recommends percona-pg-tde17; \ apt-get install -y --no-install-recommends \ percona-postgresql-contrib \ freetds-dev \ @@ -130,8 +130,8 @@ COPY prole-db-entrypoint.sh /usr/local/bin/prole-entrypoint.sh COPY 10_pg_tde_openbao.sh /docker-entrypoint-initdb.d/10_pg_tde_openbao.sh COPY prole-db-ssh-openbao.sh /usr/local/bin/prole-db-ssh-openbao.sh -COPY postgresql/*.conf /etc/postgresql/18/main/ -RUN chown postgres:postgres /etc/postgresql/18/main/*.conf +COPY postgresql/*.conf /etc/postgresql/17/main/ +RUN chown postgres:postgres /etc/postgresql/17/main/*.conf ARG PROLE_USER=prole RUN set -eux; \ diff --git a/prole/cli.py b/prole/cli.py index 9aa738c..dfa480a 100644 --- a/prole/cli.py +++ b/prole/cli.py @@ -6,9 +6,9 @@ import os import sys from pathlib import Path -from installer.core.controller import ProleController -from installer.core.env import _ensure_ansible_vault_credentials, PROJECT_ROOT -from installer.core.actions import ProleConsoleInstaller, _prepare_k3s_pipeline +from knoe.core.controller import ProleController +from knoe.core.env import _ensure_ansible_vault_credentials, PROJECT_ROOT +from knoe.core.actions import ProleConsoleInstaller, _prepare_k3s_pipeline def main(): @@ -68,7 +68,7 @@ def main(): if potential_cfg.exists(): args.config = str(potential_cfg) - from installer.config import setup_logging + from knoe.config import setup_logging setup_logging(verbose=args.verbose, debug=args.debug) @@ -98,8 +98,8 @@ def main(): if isinstance(args.log, str) else "/tmp/prole_install_output.log" ) - installer.enable_log_file(log_path) - rc = installer.run() + knoe.enable_log_file(log_path) + rc = knoe.run() if rc != 0: sys.exit(rc) if args.prepare_k3s_pipeline: @@ -109,7 +109,7 @@ def main(): # Default to ncurses if not silent and no command _ensure_ansible_vault_credentials(prompt_ui=False) try: - from installer.ncurses_installer import run_ncurses_installer + from knoe.ncurses_installer import run_ncurses_installer run_ncurses_installer(controller) except Exception as e: diff --git a/prole/deployment.py b/prole/deployment.py index 891a612..35db74d 100644 --- a/prole/deployment.py +++ b/prole/deployment.py @@ -6,9 +6,9 @@ import os import subprocess from pathlib import Path -from installer import prole_conf -from installer.core.actions import ProleInstaller -from installer.core.env import ( +from knoe import prole_conf +from knoe.core.actions import ProleInstaller +from knoe.core.env import ( _detect_ansible_topology, _sync_opentofu_pipeline, _normalize_cluster_env, @@ -54,7 +54,7 @@ class ProleDeployment(ProleInstaller): if "Global" not in self.prole_cfg_data: self.prole_cfg_data["Global"] = {} if "NAMESPACE" not in self.prole_cfg_data["Global"]: - self.prole_cfg_data["Global"]["NAMESPACE"] = "prole-db" + self.prole_cfg_data["Global"]["NAMESPACE"] = "knoe-db" def _load_prole_cfg(self) -> None: """Load prole.cfg into prole_cfg_data so _get_input can resolve values.""" @@ -140,7 +140,7 @@ class ProleDeployment(ProleInstaller): self.err("[ERROR] Cannot proceed without a valid kubeconfig.") return False - namespace = self.prole_cfg_data.get("Global", {}).get("NAMESPACE", "prole-db") + namespace = self.prole_cfg_data.get("Global", {}).get("NAMESPACE", "knoe-db") try: # Pre-render Supabase helm manifests (optional feature) @@ -240,7 +240,7 @@ class ProleDeployment(ProleInstaller): def _build_deploy_env(self) -> dict: """Build environment dict for post-apply init scripts.""" - namespace = self.prole_cfg_data.get("Global", {}).get("NAMESPACE", "prole-db") + namespace = self.prole_cfg_data.get("Global", {}).get("NAMESPACE", "knoe-db") kubeconfig = self.prole_cfg_data.get("Deployment", {}).get( "KUBECONFIG_PATH", "" ) diff --git a/scripts/generate_spec.py b/scripts/generate_spec.py index 00bb198..c8f6ab4 100755 --- a/scripts/generate_spec.py +++ b/scripts/generate_spec.py @@ -33,13 +33,13 @@ binaries = [ # Hidden imports hiddenimports = [ 'installer', - 'installer.config', - 'installer.build', - 'installer.deploy', - 'installer.screen', - 'installer.main', - 'installer.ncurses_ui', - 'installer.ncurses_installer', + 'knoe.config', + 'knoe.build', + 'knoe.deploy', + 'knoe.screen', + 'knoe.main', + 'knoe.ncurses_ui', + 'knoe.ncurses_installer', 'curses', '_curses', ] @@ -102,6 +102,6 @@ app = BUNDLE( """ if __name__ == "__main__": - with open("installer.spec", "w") as f: + with open("knoe.spec", "w") as f: f.write(SPEC_CONTENT) - print("Generated installer.spec") + print("Generated knoe.spec") diff --git a/status.py b/status.py index ad15241..9f34314 100755 --- a/status.py +++ b/status.py @@ -46,7 +46,7 @@ def _load_prole_cfg(cfg_path: Path | None = None) -> configparser.ConfigParser: """ if cfg_path is None: try: - from installer import prole_conf as prole_conf_mgr + from knoe import prole_conf as prole_conf_mgr conf_dir = prole_conf_mgr.resolve_prole_conf_dir(PROJECT_ROOT) cfg_path = prole_conf_mgr.entrypoint_path(conf_dir) @@ -54,7 +54,7 @@ def _load_prole_cfg(cfg_path: Path | None = None) -> configparser.ConfigParser: cfg_path = PROJECT_ROOT / "conf" / "prole.cfg" cp = configparser.ConfigParser(interpolation=configparser.BasicInterpolation()) try: - from installer import prole_conf as prole_conf_mgr + from knoe import prole_conf as prole_conf_mgr files = [ str(p) diff --git a/supabase/helm/render_supabase.py b/supabase/helm/render_supabase.py index 61449b7..96bc55c 100755 --- a/supabase/helm/render_supabase.py +++ b/supabase/helm/render_supabase.py @@ -26,7 +26,7 @@ REPO_ROOT = Path(__file__).resolve().parents[2] sys.path.insert(0, str(REPO_ROOT)) try: - from installer.config import _resolve_secret_value # type: ignore + from knoe.config import _resolve_secret_value # type: ignore except Exception: # pragma: no cover - fallback for minimal environments def _resolve_secret_value(value: str | None) -> str: return value or "" @@ -35,7 +35,7 @@ except Exception: # pragma: no cover - fallback for minimal environments def _read_cfg(path: Path) -> configparser.ConfigParser: parser = configparser.ConfigParser(interpolation=None) try: - from installer import prole_conf as prole_conf_mgr # type: ignore + from knoe import prole_conf as prole_conf_mgr # type: ignore files = [p for p in prole_conf_mgr.layered_cfg_files(path) if p.exists()] if files: diff --git a/tests/etc/test_init_cloudnative_pg_db_password_fallback.sh b/tests/etc/test_init_cloudnative_pg_db_password_fallback.sh new file mode 100644 index 0000000..1251f73 --- /dev/null +++ b/tests/etc/test_init_cloudnative_pg_db_password_fallback.sh @@ -0,0 +1,245 @@ +#!/usr/bin/env bash +# Regression test: init_cloudnative_pg.sh should create/update the prole-db-user +# and prole-db-superuser Kubernetes secrets from DB_PASSWORD when OpenBao is +# unavailable and the secrets are missing. + +set -euo pipefail + +SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) +PROLE_HOME=$(cd "$SCRIPT_DIR/../.." && pwd) +ETC_DIR="$PROLE_HOME/etc" +SCRIPT_UNDER_TEST="$ETC_DIR/init_cloudnative_pg.sh" + +TMP_DIR=$(mktemp -d) +trap 'rm -rf "$TMP_DIR"' EXIT +export TMP_DIR + +BIN_DIR="$TMP_DIR/bin" +mkdir -p "$BIN_DIR" + +mock_tool() { + cat <"$BIN_DIR/$1" +#!/usr/bin/env bash +echo "Mocked $1 called with \$@" >>"$TMP_DIR/mock_calls.log" +exit 0 +M_EOF + chmod +x "$BIN_DIR/$1" +} + +# Stateful kubectl mock: +# - prole-db-user starts missing +# - once we observe `kubectl create secret generic prole-db-user ...`, we mark it created +# - subsequent `kubectl get secret prole-db-user` succeeds +cat <<'K_EOF' >"$BIN_DIR/kubectl" +#!/usr/bin/env bash + +_log_file="${TMP_DIR}/mock_calls.log" +echo "Mocked kubectl called with $@" >>"${_log_file}" + +args="$*" + +_flag_user="${TMP_DIR}/flag_prole_db_user" +_flag_super="${TMP_DIR}/flag_prole_db_superuser" + +# API server readiness probes used by wait_for_apiserver_ready() +if [[ "${args}" == *"get --raw=/readyz"* || "${args}" == *"get --raw='/readyz'"* || "${args}" == *"get --raw=\"/readyz\""* ]]; then + echo "ok" + exit 0 +fi + +if [[ "${args}" == *"version --short"* ]]; then + echo "Client Version: v0.0.0" + echo "Server Version: v0.0.0" + exit 0 +fi + +# Simulate kubectl-cnpg plugin being present, but psql connectivity not yet ready. +if [[ "${args}" == *"cnpg"*"version"* ]]; then + exit 0 +fi +if [[ "${args}" == *"cnpg"*"psql"* ]]; then + exit 1 +fi + +# Secret existence simulation +if [[ "${args}" == *"get secret"*"prole-db-user"* ]]; then + if [[ -f "${_flag_user}" ]]; then + exit 0 + fi + exit 1 +fi +if [[ "${args}" == *"get secret"*"prole-db-superuser"* ]]; then + if [[ -f "${_flag_super}" ]]; then + exit 0 + fi + exit 1 +fi + +# Pretend required TLS secrets exist +if [[ "${args}" == *"get secret"*"prole-db-tls"* || "${args}" == *"get secret"*"prole-db-ca"* ]]; then + exit 0 +fi + +# CNPG webhook wait: return an endpoint IP so wait passes quickly. +if [[ "${args}" == *"get endpoints"*"cnpg-webhook-service"* ]]; then + echo "10.42.0.10" + exit 0 +fi + +# Avoid barman plugin waits +if [[ "${args}" == *"get crd"*"objectstores.barmancloud.cnpg.io"* ]]; then + exit 0 +fi +if [[ "${args}" == *"-n cnpg-system"*"get secret"*"barman-cloud-client-tls"* ]]; then + exit 0 +fi +if [[ "${args}" == *"-n cnpg-system"*"get secret"*"barman-cloud-server-tls"* ]]; then + exit 0 +fi +if [[ "${args}" == *"-n cnpg-system"*"get deploy"*"barman-cloud"* ]]; then + exit 0 +fi +if [[ "${args}" == *"-n cnpg-system"*"rollout status"*"deploy/barman-cloud"* ]]; then + exit 0 +fi +if [[ "${args}" == *"-n cnpg-system"*"get svc"*"-l cnpg.io/pluginName=barman-cloud.cloudnative-pg.io"*"-o jsonpath="*"metadata.name"* ]]; then + echo "barman-cloud" + exit 0 +fi +if [[ "${args}" == *"-n cnpg-system"*"get svc barman-cloud"*"-o jsonpath="*"metadata.name"* ]]; then + echo "barman-cloud" + exit 0 +fi +if [[ "${args}" == *"-n cnpg-system"*"get svc barman-cloud"*"pluginClientSecret"* ]]; then + echo "barman-cloud-client-tls" + exit 0 +fi +if [[ "${args}" == *"-n cnpg-system"*"get svc barman-cloud"*"pluginServerSecret"* ]]; then + echo "barman-cloud-server-tls" + exit 0 +fi +if [[ "${args}" == *"-n cnpg-system"*"get svc barman-cloud"*"pluginPort"* ]]; then + echo "9090" + exit 0 +fi +if [[ "${args}" == *"-n cnpg-system"*"get endpoints barman-cloud"* ]]; then + echo "10.42.0.11" + exit 0 +fi + +# For kubectl create secret --dry-run=client -o yaml, emit minimal YAML and record creation intent. +if [[ "${args}" == *"create secret generic prole-db-user"*"--dry-run=client"*"-o yaml"* ]]; then + touch "${_flag_user}" + echo "apiVersion: v1" + echo "kind: Secret" + exit 0 +fi +if [[ "${args}" == *"create secret generic prole-db-superuser"*"--dry-run=client"*"-o yaml"* ]]; then + touch "${_flag_super}" + echo "apiVersion: v1" + echo "kind: Secret" + exit 0 +fi + +# Allow other secret creations (cnpg-admin-key, tls, etc.) +if [[ "${args}" == *"create secret"*"--dry-run=client"*"-o yaml"* ]]; then + echo "apiVersion: v1" + echo "kind: Secret" + exit 0 +fi + +# CNPG pods listing and readiness queries +if [[ "${args}" == *"get pods"* && "${args}" == *"cnpg.io/cluster="* ]]; then + if [[ "${args}" == *"--no-headers"* ]]; then + echo "prole-db-1 1/1 Running 0 1m" + echo "prole-db-2 1/1 Running 0 1m" + echo "prole-db-3 1/1 Running 0 1m" + exit 0 + fi + if [[ "${args}" == *"-o jsonpath="* ]]; then + printf "True\nTrue\nTrue\n" + exit 0 + fi +fi + +# Treat CNPG cluster as not yet existing so initialize() doesn't try to back it up. +if [[ "${args}" == *"get cluster"* && "${args}" != *"-o jsonpath="* ]]; then + exit 1 +fi + +if [[ "${args}" == *"get cluster"*"jsonpath="*".spec.instances"* ]]; then + echo "3" + exit 0 +fi + +exit 0 +K_EOF +chmod +x "$BIN_DIR/kubectl" + +# Other tool mocks +mock_tool curl +mock_tool docker +mock_tool k3d +mock_tool skopeo +mock_tool jq + +mkdir -p "$TMP_DIR/conf" +mkdir -p "$TMP_DIR/service" +chmod 0500 "$TMP_DIR/service" || true + +TMP_HOME="$TMP_DIR/home" +mkdir -p "$TMP_HOME" + +cat <"$TMP_DIR/conf/prole.cfg" +[User] +NAMESPACE = test-ns +SERVICE_NAMESPACE = test-system +PROLE_HOME = $PROLE_HOME +PROLE_SERVICE = $TMP_DIR/service + +[Global] +DEPLOYMENT_MODE = k3d +KUBECONTEXT = test + +[Docker Build] +LOCAL_REGISTRY = localhost:5000 +LOCAL_REGISTRY_INTERNAL = k3d-prole-registry.localhost:5000 +C_EOF + +export PATH="$BIN_DIR:$PATH" +export HOME="$TMP_HOME" +export PROLE_HOME="$PROLE_HOME" +export PROLE_CONF="$TMP_DIR/conf" +export PROLE_SERVICE="$TMP_DIR/service" +export CNPG_WAIT_TIMEOUT=5 + +export PROLE_DB_USER="prole" +export DB_PASSWORD="test-password" + +set +e +bash "$SCRIPT_UNDER_TEST" --mode k3d initialize >"$TMP_DIR/stdout" 2>"$TMP_DIR/stderr" +RC=$? +set -e + +if [[ $RC -ne 0 ]]; then + echo "FAILURE: init_cloudnative_pg.sh initialize returned rc=$RC" + echo "--- stdout ---" + sed -n '1,200p' "$TMP_DIR/stdout" || true + echo "--- stderr ---" + sed -n '1,200p' "$TMP_DIR/stderr" || true + exit 1 +fi + +if ! grep -q "create secret generic prole-db-user" "$TMP_DIR/mock_calls.log"; then + echo "FAILURE: expected kubectl to create prole-db-user secret" + sed -n '1,200p' "$TMP_DIR/mock_calls.log" || true + exit 1 +fi + +if ! grep -q -- "--from-literal=password=test-password" "$TMP_DIR/mock_calls.log"; then + echo "FAILURE: expected kubectl to create secrets with DB_PASSWORD=test-password" + sed -n '1,200p' "$TMP_DIR/mock_calls.log" || true + exit 1 +fi + +echo "SUCCESS" diff --git a/tests/etc/test_init_cloudnative_pg_no_kong_in_db_namespace.sh b/tests/etc/test_init_cloudnative_pg_no_kong_in_db_namespace.sh index 0717e87..ac20cdb 100644 --- a/tests/etc/test_init_cloudnative_pg_no_kong_in_db_namespace.sh +++ b/tests/etc/test_init_cloudnative_pg_no_kong_in_db_namespace.sh @@ -41,7 +41,7 @@ metadata: name: prole-db spec: instances: 3 - imageName: prole-db:test + imageName: knoe-db:test Y_EOF cat < "$MANIFEST_DIR/kong-deployment.yaml" @@ -243,7 +243,7 @@ printf '%s' "dummy-public-key" > "$TMP_DIR/service/secrets/admin.pub" # Run initialize, but force a fast failure after ensure_prole_stack_resources # by making CNPG readiness timeout tiny. set +e -CNPG_IMAGE="prole-db:test" CNPG_WAIT_TIMEOUT=1 PROLE_PASSWD="test-password" PROLE_HOME="$ENV_HOME" \ +CNPG_IMAGE="knoe-db:test" CNPG_WAIT_TIMEOUT=1 PROLE_PASSWD="test-password" PROLE_HOME="$ENV_HOME" \ bash "$SCRIPT_UNDER_TEST" --mode k3s initialize >"$TMP_DIR/stdout" 2>"$TMP_DIR/stderr" RC=$? set -e diff --git a/tests/etc/test_init_cnpg_backup_does_not_patch_cluster_backup_spec.sh b/tests/etc/test_init_cnpg_backup_does_not_patch_cluster_backup_spec.sh index bd16a28..2459d2c 100644 --- a/tests/etc/test_init_cnpg_backup_does_not_patch_cluster_backup_spec.sh +++ b/tests/etc/test_init_cnpg_backup_does_not_patch_cluster_backup_spec.sh @@ -78,6 +78,34 @@ if [[ "${args}" == "get deployment -A -l app.kubernetes.io/name=barman-cloud -o exit 0 fi +# Barman plugin service registration + secrets (used by wait_for_barman_plugin_infra_ready) +if [[ "${args}" == *"-n cnpg-system"*"get svc"*"-l cnpg.io/pluginName=barman-cloud.cloudnative-pg.io"*"-o jsonpath="*"metadata.name"* ]]; then + echo "barman-cloud" + exit 0 +fi +if [[ "${args}" == *"-n cnpg-system"*"get svc barman-cloud"*"-o jsonpath="*"metadata.name"* ]]; then + echo "barman-cloud" + exit 0 +fi +if [[ "${args}" == *"-n cnpg-system"*"get svc barman-cloud"*"pluginClientSecret"* ]]; then + echo "barman-cloud-client-tls" + exit 0 +fi +if [[ "${args}" == *"-n cnpg-system"*"get svc barman-cloud"*"pluginServerSecret"* ]]; then + echo "barman-cloud-server-tls" + exit 0 +fi +if [[ "${args}" == *"-n cnpg-system"*"get svc barman-cloud"*"pluginPort"* ]]; then + echo "9090" + exit 0 +fi +if [[ "${args}" == *"-n cnpg-system"*"get secret barman-cloud-client-tls"* ]]; then + exit 0 +fi +if [[ "${args}" == *"-n cnpg-system"*"get secret barman-cloud-server-tls"* ]]; then + exit 0 +fi + # CNPG cluster pod selection for socket check if [[ "${args}" == *"-n test-ns"*"get pods"*"-l cnpg.io/cluster=prole-db,cnpg.io/instanceRole=primary"*"-o jsonpath={.items[0].metadata.name"* ]]; then echo "prole-db-1" @@ -108,6 +136,16 @@ if [[ "${args}" == apply*"-n"*"test-ns"*"-f"*"-"* ]]; then exit 0 fi +# ObjectStore readiness +if [[ "${args}" == *"-n test-ns"*"get objectstore prole-db-barman-objectstore"*"-o jsonpath="*".status.conditions"*"Ready"* ]]; then + echo "True" + exit 0 +fi +if [[ "${args}" == *"-n test-ns"*"get objectstore prole-db-barman-objectstore"*"-o jsonpath="*".status.phase"* ]]; then + echo "Ready" + exit 0 +fi + exit 0 K_EOF chmod +x "$BIN_DIR/kubectl" diff --git a/tests/etc/test_init_cnpg_backup_start_waits_for_plugin_socket.sh b/tests/etc/test_init_cnpg_backup_start_waits_for_plugin_socket.sh index 40e5d80..72332b6 100644 --- a/tests/etc/test_init_cnpg_backup_start_waits_for_plugin_socket.sh +++ b/tests/etc/test_init_cnpg_backup_start_waits_for_plugin_socket.sh @@ -95,6 +95,12 @@ if [[ "${args}" == *"-n cnpg-system"*"get svc barman-cloud"*"pluginPort"* ]]; th echo "9090" exit 0 fi +if [[ "${args}" == *"-n cnpg-system"*"get secret barman-cloud-client-tls"* ]]; then + exit 0 +fi +if [[ "${args}" == *"-n cnpg-system"*"get secret barman-cloud-server-tls"* ]]; then + exit 0 +fi if [[ "${args}" == *"-n cnpg-system"*"get endpoints barman-cloud"* ]]; then echo "10.42.0.11" exit 0 @@ -158,7 +164,11 @@ fi # Capture applied manifests (ObjectStore, Secret, Backup) if [[ "${args}" == apply*"-n"*"test-ns"*"-f"*"-"* ]]; then - cat >"${TMP_DIR}/applied.$(date +%s%N).yaml" + applied_file="${TMP_DIR}/applied.$(date +%s%N).yaml" + cat >"${applied_file}" + if grep -Eq '^kind:[[:space:]]*Backup\b' "${applied_file}"; then + echo "Mocked apply contained Backup" >>"${_log_file}" + fi echo "applied" exit 0 fi @@ -234,14 +244,15 @@ if [[ $RC -ne 0 ]]; then exit 1 fi -# Ensure socket checks happened before any Backup apply -socket_line=$(grep -n "exec prole-db-1 -c postgres -- test -S /plugins/barman-cloud.cloudnative-pg.io" "$TMP_DIR/mock_calls.log" | tail -n1 | cut -d: -f1 || true) -backup_apply_line=$(grep -n "apply -n test-ns -f -" "$TMP_DIR/mock_calls.log" | tail -n1 | cut -d: -f1 || true) +# Ensure plugin registration artifacts (service annotations + secrets) were checked +# before the first Backup resource was applied. +infra_line=$(grep -n "get secret barman-cloud-server-tls" "$TMP_DIR/mock_calls.log" | tail -n1 | cut -d: -f1 || true) +backup_apply_line=$(grep -n "Mocked apply contained Backup" "$TMP_DIR/mock_calls.log" | head -n1 | cut -d: -f1 || true) -if [[ -z "$socket_line" || -z "$backup_apply_line" || "$socket_line" -ge "$backup_apply_line" ]]; then - echo "FAILURE: expected plugin socket check before triggering backup apply" - echo "socket_line=$socket_line backup_apply_line=$backup_apply_line" - sed -n '1,220p' "$TMP_DIR/mock_calls.log" || true +if [[ -z "$infra_line" || -z "$backup_apply_line" || "$infra_line" -ge "$backup_apply_line" ]]; then + echo "FAILURE: expected plugin infra readiness checks before triggering Backup apply" + echo "infra_line=$infra_line backup_apply_line=$backup_apply_line" + sed -n '1,240p' "$TMP_DIR/mock_calls.log" || true exit 1 fi diff --git a/tests/etc/test_init_kdc_applies_configmap_to_prole_auth_namespace.sh b/tests/etc/test_init_kdc_applies_configmap_to_prole_auth_namespace.sh new file mode 100644 index 0000000..8f30e44 --- /dev/null +++ b/tests/etc/test_init_kdc_applies_configmap_to_prole_auth_namespace.sh @@ -0,0 +1,98 @@ +#!/usr/bin/env bash +# Regression test: etc/init_kdc.sh should ensure ConfigMap `prole-kdc-config` +# exists in the prole-auth namespace when it differs from the KDC namespace. + +set -euo pipefail + +SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) +PROLE_HOME=$(cd "$SCRIPT_DIR/../.." && pwd) +SOURCE_ETC_DIR="$PROLE_HOME/etc" + +TMP_DIR=$(mktemp -d) +trap 'rm -rf "$TMP_DIR"' EXIT + +WORK_DIR="$TMP_DIR/work" +BIN_DIR="$TMP_DIR/bin" +mkdir -p "$WORK_DIR/etc" "$WORK_DIR/conf" "$BIN_DIR" + +cp "$SOURCE_ETC_DIR/init_kdc.sh" "$WORK_DIR/etc/init_kdc.sh" +cp "$SOURCE_ETC_DIR/prole_cfg.sh" "$WORK_DIR/etc/prole_cfg.sh" +chmod +x "$WORK_DIR/etc/init_kdc.sh" + +cat < "$WORK_DIR/conf/prole.cfg" +[globals] +prole.home = $WORK_DIR +prole.mode = k3s +NAMESPACE = knoe-db +SERVICE_NAMESPACE = knoe-system +EOF + +export KUBECTL_LOG="$TMP_DIR/kubectl.log" + +cat <<'EOF' > "$BIN_DIR/kubectl" +#!/usr/bin/env bash +set -euo pipefail + +args=("$@"); printf '%s\n' "${args[*]}" >> "${KUBECTL_LOG}" + +sub="${1:-}" +case "$sub" in + config) + case "${2:-}" in + current-context) + printf '%s' "docker-desktop" + exit 0 + ;; + use-context|get-contexts) + exit 0 + ;; + esac + exit 0 + ;; + get|create|delete|rollout|wait|exec|describe|logs|patch) + # Always succeed. + if [[ "$sub" == "create" && "${2:-}" == "secret" ]]; then + cat </dev/null || true + exit 0 + ;; + *) + exit 0 + ;; +esac +EOF +chmod +x "$BIN_DIR/kubectl" + +export PATH="$BIN_DIR:$PATH" + +export PROLE_HOME="$WORK_DIR" +export PROLE_CONF="$WORK_DIR/conf" +export PROLE_MODE="k3s" + +# Provide a prebuilt image and explicit passwords to avoid docker/interactive paths. +export PROLE_KDC_IMAGE="ghcr.io/example/prole-authority:1.2.3" +export PROLE_KDC_ADMIN_PASSWORD="test-admin" +export PROLE_KDC_MASTER_PASSWORD="test-master" +export PROLE_KDC_TRUST_PASSWORD="test-trust" +export PROLE_KDC_TRUST_SHARED_PASSWORD="test-shared" + +"$WORK_DIR/etc/init_kdc.sh" update >"$TMP_DIR/stdout" 2>"$TMP_DIR/stderr" + +if ! grep -q "apply -n knoe-db" "$KUBECTL_LOG"; then + echo "FAILURE: expected init_kdc.sh to apply resources in prole-auth namespace (knoe-db)" >&2 + echo "--- kubectl log ---" >&2 + cat "$KUBECTL_LOG" >&2 + exit 1 +fi + +echo "SUCCESS" diff --git a/tests/etc/test_prole_auth_keytab_bootstrap_manifest.sh b/tests/etc/test_prole_auth_keytab_bootstrap_manifest.sh new file mode 100644 index 0000000..bfaa018 --- /dev/null +++ b/tests/etc/test_prole_auth_keytab_bootstrap_manifest.sh @@ -0,0 +1,42 @@ +#!/usr/bin/env bash +# Regression test: prole-auth manifest should not hard-require the `prole-auth-keytab` +# secret and should include a keytab bootstrap initContainer. + +set -euo pipefail + +SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) +PROLE_HOME=$(cd "$SCRIPT_DIR/../.." && pwd) + +MANIFEST="$PROLE_HOME/deploy/opentofu/k3s/manifests/prole/prole-auth-deployment.yaml" + +if [[ ! -f "$MANIFEST" ]]; then + echo "FAILURE: manifest not found: $MANIFEST" >&2 + exit 1 +fi + +if ! grep -q "^[[:space:]]*initContainers:" "$MANIFEST"; then + echo "FAILURE: expected initContainers in prole-auth deployment" >&2 + exit 1 +fi + +if ! grep -q "^[[:space:]]*- name: keytab-bootstrap" "$MANIFEST"; then + echo "FAILURE: expected initContainer named keytab-bootstrap" >&2 + exit 1 +fi + +if ! grep -Fq "secretName: prole-auth-keytab" "$MANIFEST"; then + echo "FAILURE: expected reference to secretName prole-auth-keytab (optional)" >&2 + exit 1 +fi + +if ! grep -Fq "optional: true" "$MANIFEST"; then + echo "FAILURE: expected prole-auth-keytab secret to be optional" >&2 + exit 1 +fi + +if ! grep -Fq "emptyDir: {}" "$MANIFEST"; then + echo "FAILURE: expected keytab volume to be emptyDir" >&2 + exit 1 +fi + +echo "SUCCESS" diff --git a/tests/final_comprehensive_test.sh b/tests/final_comprehensive_test.sh index e80c512..32657a2 100755 --- a/tests/final_comprehensive_test.sh +++ b/tests/final_comprehensive_test.sh @@ -25,7 +25,7 @@ run_test() { # Core functionality tests run_test "Install.py imports" python3 -c "from install import ProleController, get_resource_path" -run_test "Ncurses modules" python3 -c "from installer.ncurses_installer import run_ncurses_installer" +run_test "Ncurses modules" python3 -c "from knoe.ncurses_installer import run_ncurses_installer" run_test "Command-line interface" python3 install.py --help run_test "Makefile" make help diff --git a/tests/final_test.sh b/tests/final_test.sh index 5f8338a..ce92188 100755 --- a/tests/final_test.sh +++ b/tests/final_test.sh @@ -26,7 +26,7 @@ test_item() { # Core Features echo "CORE FEATURES" test_item "Install.py imports" python3 -c "from install import ProleController, get_resource_path" -test_item "Ncurses interface" python3 -c "from installer.ncurses_installer import run_ncurses_installer" +test_item "Ncurses interface" python3 -c "from knoe.ncurses_installer import run_ncurses_installer" test_item "Display auto-detection" python3 -c "from install import has_display" test_item "CLI arguments" python3 install.py --help diff --git a/tests/installer/conftest.py b/tests/installer/conftest.py index dc6f990..f56e649 100644 --- a/tests/installer/conftest.py +++ b/tests/installer/conftest.py @@ -16,14 +16,14 @@ _REPO_ROOT = Path(__file__).resolve().parents[2] if str(_REPO_ROOT) not in sys.path: sys.path.insert(0, str(_REPO_ROOT)) -import installer.screen as _screen_module +import knoe.screen as _screen_module @pytest.fixture(autouse=True) def reset_tkinter_mock(): """ Before each test, replace any stale tkinter MagicMock with a fresh - instance both in sys.modules and in installer.screen module namespace, + instance both in sys.modules and in knoe.screen module namespace, so TerminalConsole widget calls don't hit exhausted side-effect iterators. """ original_screen_tk = getattr(_screen_module, "tk", None) diff --git a/tests/installer/test_actions_env_setup_expands_shell_vars.py b/tests/installer/test_actions_env_setup_expands_shell_vars.py new file mode 100644 index 0000000..06e48b5 --- /dev/null +++ b/tests/installer/test_actions_env_setup_expands_shell_vars.py @@ -0,0 +1,60 @@ +from __future__ import annotations + +from pathlib import Path + + +def test_actions_save_env_to_file_expands_shell_vars(monkeypatch, tmp_path: Path): + """Regression: core env setup must expand `$HOME`/`$PROLE_HOME` for filesystem ops. + + This complements the Environment UI regression test by covering the + non-UI installer/actions code paths that can run during status checks. + """ + + monkeypatch.chdir(tmp_path) + + home_dir = tmp_path / "home" + home_dir.mkdir(parents=True, exist_ok=True) + monkeypatch.setenv("HOME", str(home_dir)) + + proj = tmp_path / "project_root" + (proj / "etc").mkdir(parents=True, exist_ok=True) + (proj / "etc" / "init-port-forward.sh").write_text( + "#!/usr/bin/env bash\necho ok\n" + ) + (proj / "etc" / "dummy.txt").write_text("ok\n") + + from knoe.core.actions import ProleInstaller + + class Dummy(ProleInstaller): + def __init__(self, project_root: Path): + self.project_root = project_root + self.inputs = {"init_cluster.cluster_env": "dev"} + self._init_shared_state() + + def _get_input(self, key: str, default: str | None = None) -> str: + if key in self.inputs: + return self.inputs[key] + return default if default is not None else "" + + dummy = Dummy(proj) + values = { + "PROLE_HOME": "$HOME/prole", + "PROLE_CONF": "$PROLE_HOME/conf", + "PROLE_DATA": "$PROLE_HOME/data", + "PROLE_LOGS": "$PROLE_HOME/logs", + "PROLE_SERVICE": "$PROLE_HOME/etc", + } + dummy._save_env_to_file(values) + + expected_home = home_dir / "prole" + assert expected_home.is_dir() + assert (expected_home / "conf").is_dir() + assert (expected_home / "data").is_dir() + assert (expected_home / "logs").is_dir() + assert (expected_home / "etc").is_dir() + assert (expected_home / "env.sh").is_file() + assert (expected_home / "init-port-forward.sh").is_file() + + # Critical assertions: we must NOT create literal `$HOME`/`$PROLE_HOME` dirs in CWD. + assert not (tmp_path / "$HOME").exists() + assert not (tmp_path / "$PROLE_HOME").exists() diff --git a/tests/installer/test_actions_helpers.py b/tests/installer/test_actions_helpers.py index d60e21e..477776f 100644 --- a/tests/installer/test_actions_helpers.py +++ b/tests/installer/test_actions_helpers.py @@ -10,7 +10,7 @@ from unittest.mock import MagicMock, patch import pytest -from installer.core.actions import ( +from knoe.core.actions import ( ProleInstaller, ProleConsoleInstaller, _configure_unbuffered_io, @@ -61,7 +61,7 @@ class TestConfigureUnbufferedIO: class TestSelectExistingKubeconfig: def test_prefers_first_working_candidate(self, tmp_path, monkeypatch): - import installer.core.actions as actions_mod + import knoe.core.actions as actions_mod bad = tmp_path / "bad.kubeconfig" good = tmp_path / "good.kubeconfig" @@ -82,7 +82,7 @@ class TestSelectExistingKubeconfig: assert _select_existing_kubeconfig([bad, good]) == good def test_falls_back_to_first_existing_when_none_work(self, tmp_path, monkeypatch): - import installer.core.actions as actions_mod + import knoe.core.actions as actions_mod first = tmp_path / "first.kubeconfig" second = tmp_path / "second.kubeconfig" @@ -425,9 +425,9 @@ class TestK3sRegistryDefaults: clear=False, ), mock.patch.object(inst, "_get_input_bool", return_value=True), - mock.patch("installer.core.actions.subprocess.run", side_effect=fake_run), - mock.patch("installer.core.actions._registry_image_ref_exists", return_value=False), - mock.patch("installer.core.actions._push_docker_image", return_value=False), + mock.patch("knoe.core.actions.subprocess.run", side_effect=fake_run), + mock.patch("knoe.core.actions._registry_image_ref_exists", return_value=False), + mock.patch("knoe.core.actions._push_docker_image", return_value=False), ): inst._step_db_build() @@ -460,8 +460,8 @@ class TestEnvPortability: inst = _TestableInstaller(project_root=tmp_path) - monkeypatch.setattr("installer.core.actions.platform.system", lambda: "Linux") - monkeypatch.setattr("installer.core.actions.Path.home", lambda: fake_home) + monkeypatch.setattr("knoe.core.actions.platform.system", lambda: "Linux") + monkeypatch.setattr("knoe.core.actions.Path.home", lambda: fake_home) with mock.patch.dict(os.environ, {}, clear=False): os.environ.pop("PROLE_HOME", None) @@ -480,7 +480,7 @@ class TestEnvPortability: (prole_home / "etc").mkdir(parents=True) inst = _TestableInstaller(project_root=tmp_path) - monkeypatch.setattr("installer.core.actions.Path.home", lambda: fake_home) + monkeypatch.setattr("knoe.core.actions.Path.home", lambda: fake_home) values = { "PROLE_HOME": str(prole_home), @@ -602,7 +602,7 @@ class TestEnsureDbK8sSecrets: return subprocess.CompletedProcess(cmd, 0, "", "") monkeypatch.setattr( - sys.modules["installer.core.actions"].subprocess, "run", fake_run + sys.modules["knoe.core.actions"].subprocess, "run", fake_run ) inst.ensure_db_k8s_secrets(ns, pw, log_fn=logs.append) @@ -658,7 +658,7 @@ class TestEnsureDbK8sSecrets: return subprocess.CompletedProcess(cmd, 0, "", "") monkeypatch.setattr( - sys.modules["installer.core.actions"].subprocess, "run", fake_run + sys.modules["knoe.core.actions"].subprocess, "run", fake_run ) inst.ensure_db_k8s_secrets(ns, "pw") @@ -735,3 +735,45 @@ class TestStepInitScripts: assert scripts.index("init_certmgr.sh") == scripts.index("init_common_services.sh") + 1 assert inst.prole_cfg_data["Initialization Scripts"]["STATUS"] == "Completed" assert inst._scripts_success is True + + +# --------------------------------------------------------------------------- +# _prepare_opentofu_pipeline +# --------------------------------------------------------------------------- + + +class TestPrepareOpenTofuPipeline: + def test_runs_for_k3d_mode_when_k3s_settings_present(self, tmp_path, monkeypatch): + import knoe.core.actions as actions_mod + + inst = _TestableSilentInstaller( + inputs={ + "init_cluster.cluster_env": "dev", + "init_cluster.mode": "k3d", + "init_password.db_namespace": "test-ns", + "init_cluster.k3s_server_url": "myrddin.prole.org:6443", + "init_cluster.k3s_token": "dummy", + }, + project_root=tmp_path, + ) + inst.prole_cfg_data.setdefault("Deployment", {}) + + expected_dir = tmp_path / "deploy" / "opentofu" / "k3s" + called = {"count": 0} + + def _fake_sync(project_root, namespace, k3s_server_url, k3s_token, log_fn=None): + called["count"] += 1 + assert project_root == tmp_path + assert namespace == "test-ns" + assert k3s_server_url.startswith("https://") + assert k3s_token == "dummy" + return expected_dir + + monkeypatch.setattr(actions_mod, "_sync_opentofu_pipeline", _fake_sync) + + inst._prepare_opentofu_pipeline() + + assert called["count"] == 1 + assert inst.prole_cfg_data["Deployment"]["OPENTOFU_PIPELINE_DIR"] == str( + expected_dir + ) diff --git a/tests/installer/test_argocd_canvas_only_page.py b/tests/installer/test_argocd_canvas_only_page.py index fd17182..4e4178e 100644 --- a/tests/installer/test_argocd_canvas_only_page.py +++ b/tests/installer/test_argocd_canvas_only_page.py @@ -4,7 +4,7 @@ from __future__ import annotations def test_show_page_argocd_config_hides_slide_area(): """Regression: `argocd_config` is a canvas-rendered page and must not be covered by `slide_area`.""" - from installer.ui.screens.base import ScreenBaseMixin + from knoe.ui.screens.base import ScreenBaseMixin class _SlideArea: def __init__(self): diff --git a/tests/installer/test_argocd_render_smoke.py b/tests/installer/test_argocd_render_smoke.py index a847bd7..3df1cf8 100644 --- a/tests/installer/test_argocd_render_smoke.py +++ b/tests/installer/test_argocd_render_smoke.py @@ -7,8 +7,8 @@ def test_render_argocd_config_page_smoke(monkeypatch): We intentionally stub `tkinter` widgets and the canvas so this test can run headless. """ - import installer.ui.screens.argocd as argocd - from installer.ui.screens.base import ScreenBaseMixin + import knoe.ui.screens.argocd as argocd + from knoe.ui.screens.base import ScreenBaseMixin class _DummyWidget: def __init__(self, *_args, **_kwargs): diff --git a/tests/installer/test_build.py b/tests/installer/test_build.py index 22ae4ec..c5f049c 100644 --- a/tests/installer/test_build.py +++ b/tests/installer/test_build.py @@ -1,5 +1,5 @@ from pathlib import Path -from installer.build import get_build_command +from knoe.build import get_build_command def test_get_build_command(): diff --git a/tests/installer/test_cfg_save_kubecontext.py b/tests/installer/test_cfg_save_kubecontext.py index 3b96546..51ce673 100644 --- a/tests/installer/test_cfg_save_kubecontext.py +++ b/tests/installer/test_cfg_save_kubecontext.py @@ -4,7 +4,7 @@ from __future__ import annotations from pathlib import Path -from installer.ui.screens.cfg import ConfigMixin +from knoe.ui.screens.cfg import ConfigMixin class _Var: @@ -69,7 +69,7 @@ class _DummyCfgApp(ConfigMixin): def test_save_prole_cfg_persists_kubecontext(tmp_path, monkeypatch): # Avoid touching real env activation during unit test. - import installer.ui.screens.cfg as cfg_mod + import knoe.ui.screens.cfg as cfg_mod monkeypatch.setattr(cfg_mod.prole_conf, "activate_environment", lambda *_a, **_k: None) diff --git a/tests/installer/test_cluster_save_triggers_status_check.py b/tests/installer/test_cluster_save_triggers_status_check.py index a8b8939..961df00 100644 --- a/tests/installer/test_cluster_save_triggers_status_check.py +++ b/tests/installer/test_cluster_save_triggers_status_check.py @@ -10,9 +10,9 @@ PROJECT_ROOT = Path(__file__).resolve().parents[2] if str(PROJECT_ROOT) not in sys.path: sys.path.insert(0, str(PROJECT_ROOT)) -import installer.ui.screens.cluster as cluster_mod -from installer.ui.screens.base import ScreenBaseMixin -from installer.ui.screens.cluster import ClusterScreenMixin +import knoe.ui.screens.cluster as cluster_mod +from knoe.ui.screens.base import ScreenBaseMixin +from knoe.ui.screens.cluster import ClusterScreenMixin class _DummyClusterSaveApp(ScreenBaseMixin, ClusterScreenMixin): @@ -90,3 +90,41 @@ def test_cluster_env_save_triggers_status_check(monkeypatch): root.destroy() except Exception: pass + + +def test_cluster_env_prod_artifacts_dir_expands_prole_home_from_cfg(monkeypatch, tmp_path): + # Some test modules replace tkinter with MagicMocks at import-time. + if isinstance(sys.modules.get("tkinter"), MagicMock): + pytest.skip("tkinter is mocked in this test run") + + # Ensure `$PROLE_HOME` is NOT available as an OS env var; it must come from config. + monkeypatch.delenv("PROLE_HOME", raising=False) + monkeypatch.chdir(tmp_path) + + root = tk.Tk() + root.withdraw() + try: + app = _DummyClusterSaveApp(root) + + # Switch to prod so the artifacts directory is validated/created. + app.cluster_env.set("prod") + + # Provide PROLE_HOME via config data only. + prole_home = tmp_path / "prole-home" + prole_home.mkdir(parents=True, exist_ok=True) + app.prole_cfg_data.setdefault("System Environment", {})["PROLE_HOME"] = str( + prole_home + ) + + # Use a path expression that previously would create a literal `$PROLE_HOME` dir. + app.prod_artifacts_path.set("$PROLE_HOME/staging") + + assert app._validate_and_save_cluster_config() is True + + assert (prole_home / "staging").is_dir() + assert not (tmp_path / "$PROLE_HOME" / "staging").exists() + finally: + try: + root.destroy() + except Exception: + pass diff --git a/tests/installer/test_cluster_screen_layout.py b/tests/installer/test_cluster_screen_layout.py index 408864f..47bf2de 100644 --- a/tests/installer/test_cluster_screen_layout.py +++ b/tests/installer/test_cluster_screen_layout.py @@ -10,8 +10,8 @@ PROJECT_ROOT = Path(__file__).resolve().parents[2] if str(PROJECT_ROOT) not in sys.path: sys.path.insert(0, str(PROJECT_ROOT)) -from installer.ui.screens.base import ScreenBaseMixin -from installer.ui.screens.cluster import ClusterScreenMixin +from knoe.ui.screens.base import ScreenBaseMixin +from knoe.ui.screens.cluster import ClusterScreenMixin class _DummyClusterApp(ScreenBaseMixin, ClusterScreenMixin): diff --git a/tests/installer/test_config.py b/tests/installer/test_config.py index d7e8d3d..d8a887b 100644 --- a/tests/installer/test_config.py +++ b/tests/installer/test_config.py @@ -1,6 +1,6 @@ import pytest from unittest.mock import patch, MagicMock -from installer.config import ( +from knoe.config import ( normalize_version, is_apple_silicon, get_docker_build_platform_args, @@ -31,7 +31,7 @@ def test_is_apple_silicon(mock_system, mock_machine): assert is_apple_silicon() is False -@patch("installer.config.is_apple_silicon") +@patch("knoe.config.is_apple_silicon") def test_get_docker_build_platform_args(mock_is_apple_silicon): mock_is_apple_silicon.return_value = True assert get_docker_build_platform_args() == ["--platform", "linux/amd64"] diff --git a/tests/installer/test_config_extended.py b/tests/installer/test_config_extended.py index b572f41..bc415ac 100644 --- a/tests/installer/test_config_extended.py +++ b/tests/installer/test_config_extended.py @@ -10,7 +10,7 @@ from unittest.mock import MagicMock, patch import pytest -from installer.config import ( +from knoe.config import ( _is_prole_secret, _is_openbao_ref, _encrypt_prole_secret, @@ -408,25 +408,25 @@ class TestDockerBuildPlatformExtended: assert result == ["--platform", "linux/arm64"] @patch.dict(os.environ, {"PROLE_DOCKER_PLATFORM": ""}, clear=False) - @patch("installer.config.is_apple_silicon", return_value=True) + @patch("knoe.config.is_apple_silicon", return_value=True) def test_dev_on_apple_silicon(self, mock_as): result = get_docker_build_platform_args("dev") assert result == ["--platform", "linux/arm64"] @patch.dict(os.environ, {"PROLE_DOCKER_PLATFORM": ""}, clear=False) - @patch("installer.config.is_apple_silicon", return_value=False) + @patch("knoe.config.is_apple_silicon", return_value=False) def test_service_env(self, mock_as): result = get_docker_build_platform_args("service") assert result == ["--platform", "linux/arm64"] @patch.dict(os.environ, {"PROLE_DOCKER_PLATFORM": ""}, clear=False) - @patch("installer.config.is_apple_silicon", return_value=False) + @patch("knoe.config.is_apple_silicon", return_value=False) def test_k3s_env(self, mock_as): result = get_docker_build_platform_args("k3s") assert result == ["--platform", "linux/arm64"] @patch.dict(os.environ, {"PROLE_DOCKER_PLATFORM": ""}, clear=False) - @patch("installer.config.is_apple_silicon", return_value=False) + @patch("knoe.config.is_apple_silicon", return_value=False) def test_no_env_no_silicon(self, mock_as): result = get_docker_build_platform_args("prod") assert result == [] diff --git a/tests/installer/test_config_helpers.py b/tests/installer/test_config_helpers.py index f04d2a0..e9c4d41 100644 --- a/tests/installer/test_config_helpers.py +++ b/tests/installer/test_config_helpers.py @@ -15,7 +15,7 @@ from pathlib import Path from unittest.mock import MagicMock, patch import pytest -from installer.config import ( +from knoe.config import ( _get_secret_key_file, _get_file_key, normalize_version, @@ -168,7 +168,7 @@ def test_try_read_ansible_vault_value_plain_text_vault_marker_skipped(tmp_path): vault.write_text("db_password: $ANSIBLE_VAULT;1.1;AES256\n") # No vault password file → returns "" with patch.dict(os.environ, {"ANSIBLE_VAULT_PASSWORD_FILE": ""}, clear=False), \ - patch("installer.config.shutil.which", return_value=None): + patch("knoe.config.shutil.which", return_value=None): result = _try_read_ansible_vault_value(vault, "db_password") assert result == "" @@ -177,7 +177,7 @@ def test_try_read_ansible_vault_value_no_password_file(tmp_path): vault = tmp_path / "prole.cfg" vault.write_text("db_password: $ANSIBLE_VAULT;1.1;AES256\n") with patch.dict(os.environ, {"ANSIBLE_VAULT_PASSWORD_FILE": ""}, clear=False), \ - patch("installer.config.shutil.which", return_value=None): + patch("knoe.config.shutil.which", return_value=None): result = _try_read_ansible_vault_value(vault, "db_password") assert result == "" @@ -193,8 +193,8 @@ def test_try_read_ansible_vault_value_with_vault_success(tmp_path): mock_result.stdout = "db_password: decryptedvalue\n" with patch.dict(os.environ, {"ANSIBLE_VAULT_PASSWORD_FILE": str(pw_file)}, clear=False), \ - patch("installer.config.shutil.which", return_value="/usr/bin/ansible-vault"), \ - patch("installer.config.subprocess.run", return_value=mock_result): + patch("knoe.config.shutil.which", return_value="/usr/bin/ansible-vault"), \ + patch("knoe.config.subprocess.run", return_value=mock_result): result = _try_read_ansible_vault_value(vault, "db_password") assert result == "decryptedvalue" @@ -210,8 +210,8 @@ def test_try_read_ansible_vault_value_vault_fails_no_inline(tmp_path): mock_result.stdout = "" with patch.dict(os.environ, {"ANSIBLE_VAULT_PASSWORD_FILE": str(pw_file)}, clear=False), \ - patch("installer.config.shutil.which", return_value="/usr/bin/ansible-vault"), \ - patch("installer.config.subprocess.run", return_value=mock_result): + patch("knoe.config.shutil.which", return_value="/usr/bin/ansible-vault"), \ + patch("knoe.config.subprocess.run", return_value=mock_result): result = _try_read_ansible_vault_value(vault, "db_password") assert result == "" @@ -242,7 +242,7 @@ def _mock_run_results(*return_codes_and_outputs): def test_get_dep_info_bin_not_found(): dep = {"id": "mytool", "name": "mytool", "bin": "mytool"} - with patch("installer.config.shutil.which", return_value=None): + with patch("knoe.config.shutil.which", return_value=None): ok, loc, ver = get_dep_info(dep) assert ok is False assert loc is None @@ -250,7 +250,7 @@ def test_get_dep_info_bin_not_found(): def test_get_dep_info_bin_found(): dep = {"id": "mytool", "name": "mytool", "bin": "mytool"} - with patch("installer.config.shutil.which", return_value="/usr/bin/mytool"): + with patch("knoe.config.shutil.which", return_value="/usr/bin/mytool"): ok, loc, ver = get_dep_info(dep) assert ok is True assert loc == "/usr/bin/mytool" @@ -260,8 +260,8 @@ def test_get_dep_info_check_cmd_success(): dep = {"id": "docker", "name": "docker", "bin": "docker", "check_cmd": "docker info"} run_result = MagicMock(returncode=0, stdout="Docker version 24.0") - with patch("installer.config.shutil.which", return_value="/usr/bin/docker"), \ - patch("installer.config.subprocess.run", return_value=run_result): + with patch("knoe.config.shutil.which", return_value="/usr/bin/docker"), \ + patch("knoe.config.subprocess.run", return_value=run_result): ok, loc, ver = get_dep_info(dep) assert ok is True assert "Docker" in (ver or "") @@ -297,11 +297,11 @@ def test_get_dep_info_prepends_linuxbrew_to_path_when_present(): return m with patch.dict(os.environ, {"PATH": "/usr/bin:/bin"}, clear=False), \ - patch("installer.config.os.path.isfile", side_effect=lambda p: p == "/home/linuxbrew/.linuxbrew/bin/brew"), \ - patch("installer.config.os.access", return_value=True), \ - patch("installer.config.os.path.isdir", side_effect=lambda p: p == "/home/linuxbrew/.linuxbrew/sbin"), \ - patch("installer.config.shutil.which", side_effect=_which), \ - patch("installer.config.subprocess.run", side_effect=_run): + patch("knoe.config.os.path.isfile", side_effect=lambda p: p == "/home/linuxbrew/.linuxbrew/bin/brew"), \ + patch("knoe.config.os.access", return_value=True), \ + patch("knoe.config.os.path.isdir", side_effect=lambda p: p == "/home/linuxbrew/.linuxbrew/sbin"), \ + patch("knoe.config.shutil.which", side_effect=_which), \ + patch("knoe.config.subprocess.run", side_effect=_run): ok, loc, ver = get_dep_info(dep) assert ok is True @@ -313,8 +313,8 @@ def test_get_dep_info_version_cmd_success(): dep = {"id": "kubectl", "name": "kubectl", "bin": "kubectl", "version_cmd": "kubectl version --client --short"} run_result = MagicMock(returncode=0, stdout="Client Version: v1.28.0") - with patch("installer.config.shutil.which", return_value="/usr/bin/kubectl"), \ - patch("installer.config.subprocess.run", return_value=run_result): + with patch("knoe.config.shutil.which", return_value="/usr/bin/kubectl"), \ + patch("knoe.config.subprocess.run", return_value=run_result): ok, loc, ver = get_dep_info(dep) assert ok is True assert ver == "Client Version: v1.28.0" @@ -322,7 +322,7 @@ def test_get_dep_info_version_cmd_success(): def test_get_dep_info_exception_returns_false(): dep = {"id": "broken", "name": "broken", "bin": "broken"} - with patch("installer.config.shutil.which", side_effect=OSError("broken")): + with patch("knoe.config.shutil.which", side_effect=OSError("broken")): ok, loc, ver = get_dep_info(dep) assert ok is False @@ -338,7 +338,7 @@ def test_load_dependencies_no_refresh(): def test_load_dependencies_with_refresh(): - with patch("installer.config.get_dep_info", + with patch("knoe.config.get_dep_info", return_value=(True, "/usr/bin/test", "1.0")): deps = load_dependencies(refresh=True) assert isinstance(deps, list) @@ -348,7 +348,7 @@ def test_load_dependencies_with_refresh(): def test_load_dependencies_refresh_exception_handled(): """If get_dep_info raises, dep.installed should be False.""" - with patch("installer.config.get_dep_info", side_effect=RuntimeError("oops")): + with patch("knoe.config.get_dep_info", side_effect=RuntimeError("oops")): deps = load_dependencies(refresh=True) for dep in deps: assert dep.get("installed") is False @@ -366,8 +366,8 @@ def test_get_ui_icon_image_path_returns_path(): def test_get_ui_icon_image_path_existing_file(tmp_path): img = tmp_path / "myicon.png" img.write_bytes(b"PNG") - with patch("installer.config.get_config_value", return_value="myicon.png"), \ - patch("installer.config.PROJECT_ROOT", tmp_path): + with patch("knoe.config.get_config_value", return_value="myicon.png"), \ + patch("knoe.config.PROJECT_ROOT", tmp_path): result = get_ui_icon_image_path() assert result == img.resolve() @@ -380,8 +380,8 @@ def test_get_ui_background_image_path_returns_path(): def test_get_ui_background_image_path_existing_file(tmp_path): img = tmp_path / "bg.png" img.write_bytes(b"PNG") - with patch("installer.config.get_config_value", return_value="bg.png"), \ - patch("installer.config.PROJECT_ROOT", tmp_path): + with patch("knoe.config.get_config_value", return_value="bg.png"), \ + patch("knoe.config.PROJECT_ROOT", tmp_path): result = get_ui_background_image_path() assert result == img.resolve() diff --git a/tests/installer/test_controller.py b/tests/installer/test_controller.py index 0e9291c..3b51ebb 100644 --- a/tests/installer/test_controller.py +++ b/tests/installer/test_controller.py @@ -11,7 +11,7 @@ from pathlib import Path from unittest.mock import MagicMock, patch import pytest -from installer.core.controller import ProleController +from knoe.core.controller import ProleController # --------------------------------------------------------------------------- @@ -35,21 +35,21 @@ def _scaffold_project(tmp_path: Path) -> None: def test_check_docker_running_success(tmp_path): ctrl = _make_controller(tmp_path) - with patch("installer.core.controller.subprocess.run") as mock_run: + with patch("knoe.core.controller.subprocess.run") as mock_run: mock_run.return_value = MagicMock(returncode=0) assert ctrl.check_docker_running() is True def test_check_docker_running_called_process_error(tmp_path): ctrl = _make_controller(tmp_path) - with patch("installer.core.controller.subprocess.run", + with patch("knoe.core.controller.subprocess.run", side_effect=subprocess.CalledProcessError(1, "docker")): assert ctrl.check_docker_running() is False def test_check_docker_running_file_not_found(tmp_path): ctrl = _make_controller(tmp_path) - with patch("installer.core.controller.subprocess.run", + with patch("knoe.core.controller.subprocess.run", side_effect=FileNotFoundError("docker not found")): assert ctrl.check_docker_running() is False @@ -263,7 +263,7 @@ def test_run_script_default_prole_home(tmp_path): # Provide env without PROLE_HOME so it falls back to Path.home() / ".prole" fake_home = tmp_path / "fakehome" fake_home.mkdir() - with patch("installer.core.controller.Path.home", return_value=fake_home): + with patch("knoe.core.controller.Path.home", return_value=fake_home): rc = ctrl.run_script("home.sh", env={}) assert rc == 0 diff --git a/tests/installer/test_controller_run_script_expands_shell_vars.py b/tests/installer/test_controller_run_script_expands_shell_vars.py new file mode 100644 index 0000000..50ba64e --- /dev/null +++ b/tests/installer/test_controller_run_script_expands_shell_vars.py @@ -0,0 +1,30 @@ +from __future__ import annotations + +from pathlib import Path + + +def test_controller_run_script_expands_shell_vars(monkeypatch, tmp_path: Path): + """Regression: `run_script` must expand `$HOME`/`$PROLE_HOME` before mkdir/copy. + + Without this, setting `PROLE_HOME=$HOME/prole` can create a literal `$HOME` + directory in the current working directory. + """ + + monkeypatch.chdir(tmp_path) + + home_dir = tmp_path / "home" + home_dir.mkdir(parents=True, exist_ok=True) + monkeypatch.setenv("HOME", str(home_dir)) + + proj = tmp_path / "project_root" + (proj / "etc").mkdir(parents=True, exist_ok=True) + (proj / "etc" / "dummy.sh").write_text("#!/usr/bin/env bash\necho ok\n") + + from knoe.core.controller import ProleController + + controller = ProleController(project_root=proj) + rc = controller.run_script("dummy.sh", env={"PROLE_HOME": "$HOME/prole"}) + assert rc == 0 + + assert (home_dir / "prole" / "etc" / "dummy.sh").is_file() + assert not (tmp_path / "$HOME").exists() diff --git a/tests/installer/test_core_classes.py b/tests/installer/test_core_classes.py index 0fc2518..74325e6 100644 --- a/tests/installer/test_core_classes.py +++ b/tests/installer/test_core_classes.py @@ -8,11 +8,11 @@ from unittest import mock import pytest -from installer.state import InstallerState -from installer.milestone import Milestone, ProgressCallback -from installer.build import get_build_command, BuildMilestone -from installer.core.controller import ProleController -from installer.core.actions import ProleConsoleInstaller +from knoe.state import InstallerState +from knoe.milestone import Milestone, ProgressCallback +from knoe.build import get_build_command, BuildMilestone +from knoe.core.controller import ProleController +from knoe.core.actions import ProleConsoleInstaller # ===== InstallerState ===== @@ -385,7 +385,7 @@ class TestProleController: class TestDependenciesMilestone: def test_init(self): - from installer.core.milestones import DependenciesMilestone + from knoe.core.milestones import DependenciesMilestone m = DependenciesMilestone() assert m.id == "dependencies" @@ -394,7 +394,7 @@ class TestDependenciesMilestone: class TestNetworkScanMilestone: def test_init(self): - from installer.core.milestones import NetworkScanMilestone + from knoe.core.milestones import NetworkScanMilestone m = NetworkScanMilestone() assert m.id == "network_scan" @@ -402,7 +402,7 @@ class TestNetworkScanMilestone: class TestEnvSetupMilestone: def test_execute(self): - from installer.core.milestones import EnvSetupMilestone + from knoe.core.milestones import EnvSetupMilestone m = EnvSetupMilestone() state = InstallerState( @@ -422,7 +422,7 @@ class TestEnvSetupMilestone: class TestSecretManagementMilestone: def test_execute_plain(self): - from installer.core.milestones import SecretManagementMilestone + from knoe.core.milestones import SecretManagementMilestone m = SecretManagementMilestone() state = InstallerState( @@ -435,7 +435,7 @@ class TestSecretManagementMilestone: assert state.inputs["init_password.db_password_confirm"] == "plaintext123" def test_execute_no_passwords(self): - from installer.core.milestones import SecretManagementMilestone + from knoe.core.milestones import SecretManagementMilestone m = SecretManagementMilestone() state = InstallerState(inputs={}) @@ -444,7 +444,7 @@ class TestSecretManagementMilestone: class TestDatabaseCreationMilestone: def test_execute_generates_password(self): - from installer.core.milestones import DatabaseCreationMilestone + from knoe.core.milestones import DatabaseCreationMilestone m = DatabaseCreationMilestone() state = InstallerState( @@ -457,7 +457,7 @@ class TestDatabaseCreationMilestone: assert state.config_data["Database Creation"]["DB_NAME"] == "testns" def test_execute_with_password(self): - from installer.core.milestones import DatabaseCreationMilestone + from knoe.core.milestones import DatabaseCreationMilestone m = DatabaseCreationMilestone() state = InstallerState( @@ -471,7 +471,7 @@ class TestDatabaseCreationMilestone: assert state.inputs["init_password.db_password"] == "mypassword" def test_execute_unresolvable_openbao(self): - from installer.core.milestones import DatabaseCreationMilestone + from knoe.core.milestones import DatabaseCreationMilestone m = DatabaseCreationMilestone() state = InstallerState( @@ -489,7 +489,7 @@ class TestDatabaseCreationMilestone: class TestDockerBuildMilestone: def test_disabled(self): - from installer.core.milestones import DockerBuildMilestone + from knoe.core.milestones import DockerBuildMilestone m = DockerBuildMilestone() state = InstallerState( @@ -501,7 +501,7 @@ class TestDockerBuildMilestone: class TestDeploymentMilestone: def test_init(self): - from installer.core.milestones import DeploymentMilestone + from knoe.core.milestones import DeploymentMilestone m = DeploymentMilestone() assert m.id == "deployment" diff --git a/tests/installer/test_db_build_registry_status_k3s.py b/tests/installer/test_db_build_registry_status_k3s.py index 0e975ff..c1180cf 100644 --- a/tests/installer/test_db_build_registry_status_k3s.py +++ b/tests/installer/test_db_build_registry_status_k3s.py @@ -5,8 +5,8 @@ import json import time -import installer.ui.screens.database as database_mod -from installer.ui.screens.database import DatabaseScreenMixin +import knoe.ui.screens.database as database_mod +from knoe.ui.screens.database import DatabaseScreenMixin class _DummyCanvas: diff --git a/tests/installer/test_deploy_helpers.py b/tests/installer/test_deploy_helpers.py index 64f5744..1a9f660 100644 --- a/tests/installer/test_deploy_helpers.py +++ b/tests/installer/test_deploy_helpers.py @@ -11,7 +11,7 @@ from pathlib import Path from unittest.mock import MagicMock, patch import pytest -from installer.deploy import check_xcode_tools, build_prole_app_core +from knoe.deploy import check_xcode_tools, build_prole_app_core # --------------------------------------------------------------------------- @@ -19,40 +19,40 @@ from installer.deploy import check_xcode_tools, build_prole_app_core # --------------------------------------------------------------------------- def test_check_xcode_tools_not_darwin(): - with patch("installer.deploy.platform.system", return_value="Linux"): + with patch("knoe.deploy.platform.system", return_value="Linux"): assert check_xcode_tools() is False def test_check_xcode_tools_not_darwin_windows(): - with patch("installer.deploy.platform.system", return_value="Windows"): + with patch("knoe.deploy.platform.system", return_value="Windows"): assert check_xcode_tools() is False def test_check_xcode_tools_darwin_success(): mock_result = MagicMock() mock_result.returncode = 0 - with patch("installer.deploy.platform.system", return_value="Darwin"), \ - patch("installer.deploy.subprocess.run", return_value=mock_result): + with patch("knoe.deploy.platform.system", return_value="Darwin"), \ + patch("knoe.deploy.subprocess.run", return_value=mock_result): assert check_xcode_tools() is True def test_check_xcode_tools_darwin_failure(): mock_result = MagicMock() mock_result.returncode = 1 - with patch("installer.deploy.platform.system", return_value="Darwin"), \ - patch("installer.deploy.subprocess.run", return_value=mock_result): + with patch("knoe.deploy.platform.system", return_value="Darwin"), \ + patch("knoe.deploy.subprocess.run", return_value=mock_result): assert check_xcode_tools() is False def test_check_xcode_tools_darwin_exception(): - with patch("installer.deploy.platform.system", return_value="Darwin"), \ - patch("installer.deploy.subprocess.run", side_effect=FileNotFoundError("xcrun not found")): + with patch("knoe.deploy.platform.system", return_value="Darwin"), \ + patch("knoe.deploy.subprocess.run", side_effect=FileNotFoundError("xcrun not found")): assert check_xcode_tools() is False def test_check_xcode_tools_darwin_timeout(): - with patch("installer.deploy.platform.system", return_value="Darwin"), \ - patch("installer.deploy.subprocess.run", side_effect=subprocess.TimeoutExpired("xcrun", 10)): + with patch("knoe.deploy.platform.system", return_value="Darwin"), \ + patch("knoe.deploy.subprocess.run", side_effect=subprocess.TimeoutExpired("xcrun", 10)): assert check_xcode_tools() is False @@ -61,14 +61,14 @@ def test_check_xcode_tools_darwin_timeout(): # --------------------------------------------------------------------------- def test_build_prole_app_core_not_darwin(tmp_path): - with patch("installer.deploy.platform.system", return_value="Linux"): + with patch("knoe.deploy.platform.system", return_value="Linux"): with pytest.raises(Exception, match="macOS"): build_prole_app_core(tmp_path) def test_build_prole_app_core_no_xcode(tmp_path): - with patch("installer.deploy.platform.system", return_value="Darwin"), \ - patch("installer.deploy.check_xcode_tools", return_value=False): + with patch("knoe.deploy.platform.system", return_value="Darwin"), \ + patch("knoe.deploy.check_xcode_tools", return_value=False): with pytest.raises(Exception, match="[Xx]code"): build_prole_app_core(tmp_path) @@ -77,8 +77,8 @@ def test_build_prole_app_core_no_build_script(tmp_path): # xcode ok but prole-app/build.sh missing prole_app = tmp_path / "prole-app" prole_app.mkdir() - with patch("installer.deploy.platform.system", return_value="Darwin"), \ - patch("installer.deploy.check_xcode_tools", return_value=True): + with patch("knoe.deploy.platform.system", return_value="Darwin"), \ + patch("knoe.deploy.check_xcode_tools", return_value=True): with pytest.raises(Exception, match="[Bb]uild script"): build_prole_app_core(tmp_path) @@ -92,9 +92,9 @@ def test_build_prole_app_core_build_fails(tmp_path): mock_result.returncode = 1 mock_result.stderr = "build error" mock_result.stdout = "" - with patch("installer.deploy.platform.system", return_value="Darwin"), \ - patch("installer.deploy.check_xcode_tools", return_value=True), \ - patch("installer.deploy.subprocess.run", return_value=mock_result): + with patch("knoe.deploy.platform.system", return_value="Darwin"), \ + patch("knoe.deploy.check_xcode_tools", return_value=True), \ + patch("knoe.deploy.subprocess.run", return_value=mock_result): with pytest.raises(Exception, match="[Bb]uild failed"): build_prole_app_core(tmp_path) @@ -108,9 +108,9 @@ def test_build_prole_app_core_app_missing_after_build(tmp_path): mock_result.returncode = 0 mock_result.stderr = "" mock_result.stdout = "Build OK" - with patch("installer.deploy.platform.system", return_value="Darwin"), \ - patch("installer.deploy.check_xcode_tools", return_value=True), \ - patch("installer.deploy.subprocess.run", return_value=mock_result): + with patch("knoe.deploy.platform.system", return_value="Darwin"), \ + patch("knoe.deploy.check_xcode_tools", return_value=True), \ + patch("knoe.deploy.subprocess.run", return_value=mock_result): # dist/Prole.app is not created → should raise with pytest.raises(Exception, match="[Pp]role.app"): build_prole_app_core(tmp_path) @@ -125,8 +125,8 @@ def test_build_prole_app_core_success(tmp_path): build_sh.write_text("#!/bin/bash\nexit 0\n") mock_result = MagicMock() mock_result.returncode = 0 - with patch("installer.deploy.platform.system", return_value="Darwin"), \ - patch("installer.deploy.check_xcode_tools", return_value=True), \ - patch("installer.deploy.subprocess.run", return_value=mock_result): + with patch("knoe.deploy.platform.system", return_value="Darwin"), \ + patch("knoe.deploy.check_xcode_tools", return_value=True), \ + patch("knoe.deploy.subprocess.run", return_value=mock_result): result = build_prole_app_core(tmp_path) assert result == app_path diff --git a/tests/installer/test_deployment.py b/tests/installer/test_deployment.py index 6cc7458..82afec1 100644 --- a/tests/installer/test_deployment.py +++ b/tests/installer/test_deployment.py @@ -87,14 +87,14 @@ class TestProleDeploymentInit: ctrl = _make_controller(tmp_path) with mock.patch.dict(os.environ, {"PROLE_CONF": str(conf_dir)}): dep = ProleDeployment(ctrl, tmp_path) - assert dep.prole_cfg_data["Global"]["NAMESPACE"] == "prole-db" + assert dep.prole_cfg_data["Global"]["NAMESPACE"] == "knoe-db" def test_init_no_cfg_file(self, tmp_path): """When no prole.cfg exists, it still initialises.""" ctrl = _make_controller(tmp_path) with mock.patch.dict(os.environ, {"PROLE_CONF": ""}, clear=False): dep = ProleDeployment(ctrl, tmp_path) - assert dep.prole_cfg_data["Global"]["NAMESPACE"] == "prole-db" + assert dep.prole_cfg_data["Global"]["NAMESPACE"] == "knoe-db" def test_load_cfg_from_directory(self, tmp_path): """PROLE_CONF pointing to a directory finds prole.cfg inside.""" diff --git a/tests/installer/test_env_helpers.py b/tests/installer/test_env_helpers.py index 77014ab..4b2df0a 100644 --- a/tests/installer/test_env_helpers.py +++ b/tests/installer/test_env_helpers.py @@ -1,4 +1,4 @@ -"""Tests for installer.core.env pure utility functions.""" +"""Tests for knoe.core.env pure utility functions.""" from __future__ import annotations @@ -14,7 +14,7 @@ import pytest # --------------------------------------------------------------------------- # env.py helpers # --------------------------------------------------------------------------- -from installer.core.env import ( +from knoe.core.env import ( _bool_str, _build_required_port_forwards, _clean_yaml_value, @@ -56,7 +56,7 @@ from installer.core.env import ( # --------------------------------------------------------------------------- # config.py helpers # --------------------------------------------------------------------------- -from installer.config import ( +from knoe.config import ( _parse_bool as cfg_parse_bool, _expand_path as cfg_expand_path, _expand_cfg_value as cfg_expand_cfg_value, @@ -601,7 +601,7 @@ class TestFindKubeconfig: self, tmp_path, monkeypatch ): # If both exist, prefer the Ansible-fetched kubeconfig in project root. - import installer.core.env as env_mod + import knoe.core.env as env_mod fake_home = tmp_path / "home" fake_home.mkdir(parents=True, exist_ok=True) @@ -726,7 +726,7 @@ class TestLoadProperties: class TestWriteK3sKubeconfig: def test_basic(self, tmp_path): - with mock.patch("installer.config.PROJECT_ROOT", tmp_path), mock.patch.dict( + with mock.patch("knoe.config.PROJECT_ROOT", tmp_path), mock.patch.dict( os.environ, {"PROLE_SERVICE": "", "PROLE_HOME": ""}, clear=False ): result = _write_k3s_kubeconfig("https://server:6443", "mytoken") @@ -736,7 +736,7 @@ class TestWriteK3sKubeconfig: assert "mytoken" in content def test_no_scheme(self, tmp_path): - with mock.patch("installer.config.PROJECT_ROOT", tmp_path), mock.patch.dict( + with mock.patch("knoe.config.PROJECT_ROOT", tmp_path), mock.patch.dict( os.environ, {"PROLE_SERVICE": "", "PROLE_HOME": ""}, clear=False ): result = _write_k3s_kubeconfig("server:6443", "tok") @@ -760,7 +760,7 @@ class TestMergeKubeconfig: " name: prole-k3s\ncurrent-context: prole-k3s\n" "users:\n- name: prole-k3s\n user:\n token: tok123\n" ) - with mock.patch("installer.config.Path.home", return_value=tmp_path): + with mock.patch("knoe.config.Path.home", return_value=tmp_path): result = _merge_kubeconfig(str(standalone)) if result: merged = (kube_dir / "config").read_text() @@ -788,7 +788,7 @@ class TestMergeKubeconfig: " name: prole-k3s\ncurrent-context: prole-k3s\n" "users:\n- name: prole-k3s\n user:\n token: tok123\n" ) - with mock.patch("installer.config.Path.home", return_value=tmp_path): + with mock.patch("knoe.config.Path.home", return_value=tmp_path): result = _merge_kubeconfig(str(standalone)) if result: merged = existing.read_text() @@ -800,7 +800,7 @@ class TestMergeKubeconfig: standalone = tmp_path / "standalone.yaml" standalone.write_text("apiVersion: v1\nkind: Config\n") with mock.patch( - "installer.config.Path.home", return_value=tmp_path + "knoe.config.Path.home", return_value=tmp_path ), mock.patch("subprocess.run", side_effect=FileNotFoundError): result = _merge_kubeconfig(str(standalone)) assert result is False @@ -856,14 +856,14 @@ class TestUpdateProleCfgValue: cfg_file = tmp_path / "conf" / "prole.cfg" cfg_file.parent.mkdir(parents=True) cfg_file.write_text("[Global]\nFOO = bar\n") - with mock.patch("installer.config.PROJECT_ROOT", tmp_path): + with mock.patch("knoe.config.PROJECT_ROOT", tmp_path): _update_prole_cfg_value("NewSection", "KEY", "VALUE") content = cfg_file.read_text() assert "NewSection" in content assert "KEY" in content def test_missing_cfg(self, tmp_path): - with mock.patch("installer.config.PROJECT_ROOT", tmp_path): + with mock.patch("knoe.config.PROJECT_ROOT", tmp_path): _update_prole_cfg_value("Sec", "K", "V") # should not raise def test_writes_through_prole_conf_symlink(self, tmp_path): @@ -913,7 +913,7 @@ class TestPlatformHelpers: class TestRenderProleCfg: def test_minimal(self): - from installer.core.env import _render_prole_cfg + from knoe.core.env import _render_prole_cfg result = _render_prole_cfg( inputs={ @@ -929,7 +929,7 @@ class TestRenderProleCfg: assert "test-ns" in result or "NAMESPACE" in result def test_with_sections(self): - from installer.core.env import _render_prole_cfg + from knoe.core.env import _render_prole_cfg result = _render_prole_cfg( inputs={ @@ -959,7 +959,7 @@ class TestRenderProleCfg: def test_does_not_inject_placeholder_filesystem_paths(self): """New policy: prole.cfg should not introduce `${...}` placeholders for filesystem paths.""" - from installer.core.env import _render_prole_cfg + from knoe.core.env import _render_prole_cfg result = _render_prole_cfg( inputs={ @@ -1029,7 +1029,7 @@ class TestRegistryManifestProbe: monkeypatch.setattr(http.client, "HTTPConnection", _Conn) - assert _registry_image_ref_exists("myrddin.prole.org:5000/prole-db:latest") is True + assert _registry_image_ref_exists("myrddin.prole.org:5000/knoe-db:latest") is True assert requests and requests[0][0] == "HEAD" assert all(m != "GET" for m, _ in requests) @@ -1057,7 +1057,7 @@ class TestRegistryManifestProbe: return None monkeypatch.setattr(http.client, "HTTPConnection", _Conn) - assert _registry_image_ref_exists("myrddin.prole.org:5000/prole-db:latest") is False + assert _registry_image_ref_exists("myrddin.prole.org:5000/knoe-db:latest") is False def test_head_405_falls_back_to_get(self, monkeypatch): import http.client @@ -1089,5 +1089,5 @@ class TestRegistryManifestProbe: return None monkeypatch.setattr(http.client, "HTTPConnection", _Conn) - assert _registry_image_ref_exists("myrddin.prole.org:5000/prole-db:latest") is True + assert _registry_image_ref_exists("myrddin.prole.org:5000/knoe-db:latest") is True assert requests == ["HEAD", "GET"] diff --git a/tests/installer/test_environment_screen_expands_shell_vars.py b/tests/installer/test_environment_screen_expands_shell_vars.py index 790b135..26fcfe5 100644 --- a/tests/installer/test_environment_screen_expands_shell_vars.py +++ b/tests/installer/test_environment_screen_expands_shell_vars.py @@ -25,7 +25,7 @@ def test_environment_screen_expands_shell_vars_for_filesystem(monkeypatch, tmp_p (proj / "etc").mkdir(parents=True, exist_ok=True) (proj / "etc" / "init-port-forward.sh").write_text("#!/usr/bin/env bash\necho ok\n") - import installer.ui.screens.environment as env_screen + import knoe.ui.screens.environment as env_screen monkeypatch.setattr(env_screen, "PROJECT_ROOT", proj) diff --git a/tests/installer/test_gitea_init_script_present.py b/tests/installer/test_gitea_init_script_present.py index 8faffbb..ff83169 100644 --- a/tests/installer/test_gitea_init_script_present.py +++ b/tests/installer/test_gitea_init_script_present.py @@ -1,6 +1,6 @@ from __future__ import annotations -from installer.core.env import PROJECT_ROOT +from knoe.core.env import PROJECT_ROOT def test_init_gitea_script_exists(): diff --git a/tests/installer/test_k3s_registry_hostport_resolution.py b/tests/installer/test_k3s_registry_hostport_resolution.py index a44ae3b..1447d7e 100644 --- a/tests/installer/test_k3s_registry_hostport_resolution.py +++ b/tests/installer/test_k3s_registry_hostport_resolution.py @@ -6,8 +6,8 @@ from pathlib import Path import pytest -import installer.ui.screens.docker as docker_mod -from installer.ui.screens.docker import DockerScreenMixin +import knoe.ui.screens.docker as docker_mod +from knoe.ui.screens.docker import DockerScreenMixin class _DummyDockerScreen(DockerScreenMixin): diff --git a/tests/installer/test_milestones.py b/tests/installer/test_milestones.py index b41bc18..3c470ed 100644 --- a/tests/installer/test_milestones.py +++ b/tests/installer/test_milestones.py @@ -1,7 +1,7 @@ import unittest from unittest.mock import MagicMock, patch, ANY from pathlib import Path -from installer.core.milestones import ( +from knoe.core.milestones import ( DependenciesMilestone, NetworkScanMilestone, EnvSetupMilestone, @@ -10,8 +10,8 @@ from installer.core.milestones import ( InitializationScriptsMilestone, DeploymentMilestone, ) -from installer.state import InstallerState -from installer.core.controller import ProleController +from knoe.state import InstallerState +from knoe.core.controller import ProleController class TestMilestones(unittest.TestCase): @@ -21,7 +21,7 @@ class TestMilestones(unittest.TestCase): self.state.inputs["network_scan.run"] = "True" self.state.controller = MagicMock(spec=ProleController) - @patch("installer.config.get_dep_info") + @patch("knoe.config.get_dep_info") @patch("subprocess.run") def test_dependencies_milestone_all_installed(self, mock_run, mock_get_dep_info): # Mock all dependencies as installed @@ -38,7 +38,7 @@ class TestMilestones(unittest.TestCase): ) progress_mock.assert_called() - @patch("installer.config.get_dep_info") + @patch("knoe.config.get_dep_info") @patch("subprocess.Popen") def test_dependencies_milestone_install_missing( self, mock_popen, mock_get_dep_info @@ -75,8 +75,8 @@ class TestMilestones(unittest.TestCase): # Check that it tried to install brew mock_popen.assert_called() - @patch("installer.config.get_resource_path") - @patch("installer.core.milestones.run_streaming_cmd") + @patch("knoe.config.get_resource_path") + @patch("knoe.core.milestones.run_streaming_cmd") def test_network_scan_milestone(self, mock_run_streaming_cmd, mock_get_resource): mock_binary = MagicMock() mock_binary.exists.return_value = True @@ -116,7 +116,7 @@ class TestMilestones(unittest.TestCase): "test-ns", ) - @patch("installer.core.milestones.inst_config._resolve_secret_value") + @patch("knoe.core.milestones.inst_config._resolve_secret_value") def test_secret_management_milestone(self, mock_resolve): mock_resolve.side_effect = lambda x: f"resolved-{x}" if "secret" in x else x @@ -184,7 +184,7 @@ class TestMilestones(unittest.TestCase): ["k3d", "cluster", "list", "--no-headers"], capture_output=True, text=True ) - @patch("installer.core.milestones.InitializationScriptsMilestone._run_cmd") + @patch("knoe.core.milestones.InitializationScriptsMilestone._run_cmd") @patch("subprocess.run") def test_init_scripts_milestone(self, mock_subprocess_run, mock_run_cmd): self.state.controller.run_script.return_value = 0 diff --git a/tests/installer/test_milestones_extended.py b/tests/installer/test_milestones_extended.py index aaec4fc..2193070 100644 --- a/tests/installer/test_milestones_extended.py +++ b/tests/installer/test_milestones_extended.py @@ -12,7 +12,7 @@ from pathlib import Path from unittest.mock import MagicMock, patch, call import pytest -from installer.core.milestones import ( +from knoe.core.milestones import ( _stream_line, DatabaseCreationMilestone, DockerBuildMilestone, @@ -23,8 +23,8 @@ from installer.core.milestones import ( DependenciesMilestone, InitializationScriptsMilestone, ) -from installer.state import InstallerState -from installer.core.controller import ProleController +from knoe.state import InstallerState +from knoe.core.controller import ProleController # --------------------------------------------------------------------------- @@ -70,8 +70,8 @@ def test_stream_line_tolerates_broken_stdout(): def test_database_creation_defaults(): state = _make_state() milestone = DatabaseCreationMilestone() - with patch("installer.core.milestones.inst_config._encrypt_cfg_secret", return_value="enc:xxx"), \ - patch("installer.core.milestones.inst_config._is_prole_secret", return_value=False): + with patch("knoe.core.milestones.inst_config._encrypt_cfg_secret", return_value="enc:xxx"), \ + patch("knoe.core.milestones.inst_config._is_prole_secret", return_value=False): milestone.execute(state) assert state.config_data.get("Database Creation", {}).get("DB_NAME") == "default" @@ -84,8 +84,8 @@ def test_database_creation_with_provided_values(): "init_password.generate_ssh_key": "False", }) milestone = DatabaseCreationMilestone() - with patch("installer.core.milestones.inst_config._encrypt_cfg_secret", return_value="enc:xxx"), \ - patch("installer.core.milestones.inst_config._is_prole_secret", return_value=False): + with patch("knoe.core.milestones.inst_config._encrypt_cfg_secret", return_value="enc:xxx"), \ + patch("knoe.core.milestones.inst_config._is_prole_secret", return_value=False): milestone.execute(state) db = state.config_data.get("Database Creation", {}) assert db["DB_USER"] == "alice" @@ -98,8 +98,8 @@ def test_database_creation_generates_password_when_missing(): "init_password.generate_ssh_key": "False", }) milestone = DatabaseCreationMilestone() - with patch("installer.core.milestones.inst_config._encrypt_cfg_secret", return_value="enc:xxx"), \ - patch("installer.core.milestones.inst_config._is_prole_secret", return_value=False): + with patch("knoe.core.milestones.inst_config._encrypt_cfg_secret", return_value="enc:xxx"), \ + patch("knoe.core.milestones.inst_config._is_prole_secret", return_value=False): milestone.execute(state) # Password should have been generated and stored assert state.inputs.get("init_password.db_password") @@ -113,9 +113,9 @@ def test_database_creation_resolves_secret_ref(): "init_password.generate_ssh_key": "False", }) milestone = DatabaseCreationMilestone() - with patch("installer.core.milestones.inst_config._resolve_secret_value", return_value="${openbao:kv/prole/db}"), \ - patch("installer.core.milestones.inst_config._encrypt_cfg_secret", return_value="enc:xxx"), \ - patch("installer.core.milestones.inst_config._is_prole_secret", return_value=False): + with patch("knoe.core.milestones.inst_config._resolve_secret_value", return_value="${openbao:kv/prole/db}"), \ + patch("knoe.core.milestones.inst_config._encrypt_cfg_secret", return_value="enc:xxx"), \ + patch("knoe.core.milestones.inst_config._is_prole_secret", return_value=False): milestone.execute(state) # Unresolvable ref → new password generated pw = state.inputs.get("init_password.db_password", "") @@ -129,9 +129,9 @@ def test_database_creation_resolves_secret_ref_success(): "init_password.generate_ssh_key": "False", }) milestone = DatabaseCreationMilestone() - with patch("installer.core.milestones.inst_config._resolve_secret_value", return_value="realpassword"), \ - patch("installer.core.milestones.inst_config._encrypt_cfg_secret", return_value="enc:xxx"), \ - patch("installer.core.milestones.inst_config._is_prole_secret", return_value=False): + with patch("knoe.core.milestones.inst_config._resolve_secret_value", return_value="realpassword"), \ + patch("knoe.core.milestones.inst_config._encrypt_cfg_secret", return_value="enc:xxx"), \ + patch("knoe.core.milestones.inst_config._is_prole_secret", return_value=False): milestone.execute(state) assert state.inputs["init_password.db_password"] == "realpassword" @@ -140,8 +140,8 @@ def test_database_creation_with_progress(): state = _make_state(**{"init_password.generate_ssh_key": "False"}) milestone = DatabaseCreationMilestone() progress = MagicMock() - with patch("installer.core.milestones.inst_config._encrypt_cfg_secret", return_value="enc:xxx"), \ - patch("installer.core.milestones.inst_config._is_prole_secret", return_value=False): + with patch("knoe.core.milestones.inst_config._encrypt_cfg_secret", return_value="enc:xxx"), \ + patch("knoe.core.milestones.inst_config._is_prole_secret", return_value=False): milestone.execute(state, progress=progress) progress.assert_called() @@ -152,8 +152,8 @@ def test_database_creation_uses_env_namespace_fallback(): "init_password.generate_ssh_key": "False", }) milestone = DatabaseCreationMilestone() - with patch("installer.core.milestones.inst_config._encrypt_cfg_secret", return_value="enc:xxx"), \ - patch("installer.core.milestones.inst_config._is_prole_secret", return_value=False): + with patch("knoe.core.milestones.inst_config._encrypt_cfg_secret", return_value="enc:xxx"), \ + patch("knoe.core.milestones.inst_config._is_prole_secret", return_value=False): milestone.execute(state) assert state.config_data["Database Creation"]["NAMESPACE"] == "staging" @@ -175,7 +175,7 @@ def test_docker_build_runs_script(): "init_cluster.cluster_env": "dev", }) milestone = DockerBuildMilestone() - with patch("installer.core.env._normalize_cluster_env", return_value="dev"): + with patch("knoe.core.env._normalize_cluster_env", return_value="dev"): milestone.execute(state) state.controller.run_script.assert_called_once() args, kwargs = state.controller.run_script.call_args @@ -192,7 +192,7 @@ def test_docker_build_non_dev_env_adds_registry(): "init_scripts.run_scripts": "False", }) milestone = DockerBuildMilestone() - with patch("installer.core.env._normalize_cluster_env", return_value="prod"): + with patch("knoe.core.env._normalize_cluster_env", return_value="prod"): milestone.execute(state) state.controller.run_script.assert_called_once() _, kwargs = state.controller.run_script.call_args @@ -209,7 +209,7 @@ def test_docker_build_non_dev_env_skips_when_init_scripts_enabled(): "init_scripts.run_scripts": "True", }) milestone = DockerBuildMilestone() - with patch("installer.core.env._normalize_cluster_env", return_value="prod"): + with patch("knoe.core.env._normalize_cluster_env", return_value="prod"): milestone.execute(state) state.controller.run_script.assert_not_called() @@ -218,7 +218,7 @@ def test_docker_build_failed_rc(): state = _make_state(**{"init_db_build.run_build": "True"}) state.controller.run_script.return_value = 1 milestone = DockerBuildMilestone() - with patch("installer.core.env._normalize_cluster_env", return_value="dev"): + with patch("knoe.core.env._normalize_cluster_env", return_value="dev"): # Should not raise — just logs error milestone.execute(state) @@ -227,7 +227,7 @@ def test_docker_build_with_progress(): state = _make_state(**{"init_db_build.run_build": "True"}) milestone = DockerBuildMilestone() progress = MagicMock() - with patch("installer.core.env._normalize_cluster_env", return_value="dev"): + with patch("knoe.core.env._normalize_cluster_env", return_value="dev"): milestone.execute(state, progress=progress) progress.assert_called() @@ -397,9 +397,9 @@ def test_supabase_image_preload_success(tmp_path): milestone = SupabaseImagePreloadMilestone() with patch.object(milestone, "_get_script_env", return_value={}), \ - patch("installer.core.milestones.subprocess.run", side_effect=capture_run), \ - patch("installer.core.env._normalize_cluster_env", return_value="dev"), \ - patch("installer.core.env._parse_bool", return_value=False): + patch("knoe.core.milestones.subprocess.run", side_effect=capture_run), \ + patch("knoe.core.env._normalize_cluster_env", return_value="dev"), \ + patch("knoe.core.env._parse_bool", return_value=False): milestone.execute(state) assert state.config_data["Supabase"]["IMAGES_STATUS"] == "Prepared" @@ -422,9 +422,9 @@ def test_supabase_image_preload_failure_raises(tmp_path): milestone = SupabaseImagePreloadMilestone() with patch.object(milestone, "_get_script_env", return_value={}), \ - patch("installer.core.milestones.subprocess.run", return_value=mock_result), \ - patch("installer.core.env._normalize_cluster_env", return_value="dev"), \ - patch("installer.core.env._parse_bool", return_value=False): + patch("knoe.core.milestones.subprocess.run", return_value=mock_result), \ + patch("knoe.core.env._normalize_cluster_env", return_value="dev"), \ + patch("knoe.core.env._parse_bool", return_value=False): with pytest.raises(Exception, match="[Pp]reload"): milestone.execute(state) @@ -443,8 +443,8 @@ def test_supabase_image_preload_skips_for_k8s_mode(tmp_path): milestone = SupabaseImagePreloadMilestone() with patch.object(milestone, "_get_script_env", return_value={}), \ - patch("installer.core.milestones.subprocess.run") as mock_run, \ - patch("installer.core.env._normalize_cluster_env", return_value="prod"): + patch("knoe.core.milestones.subprocess.run") as mock_run, \ + patch("knoe.core.env._normalize_cluster_env", return_value="prod"): milestone.execute(state) assert state.config_data["Supabase"]["IMAGES_STATUS"] == "Skipped" @@ -495,9 +495,9 @@ def test_supabase_enabled_success(tmp_path): mock_result.returncode = 0 milestone = SupabaseMilestone() with patch.object(milestone, "_get_script_env", return_value={}), \ - patch("installer.core.milestones.subprocess.run", return_value=mock_result), \ - patch("installer.core.env._normalize_cluster_env", return_value="dev"), \ - patch("installer.core.env._parse_bool", return_value=False): + patch("knoe.core.milestones.subprocess.run", return_value=mock_result), \ + patch("knoe.core.env._normalize_cluster_env", return_value="dev"), \ + patch("knoe.core.env._parse_bool", return_value=False): milestone.execute(state) assert state.config_data["Supabase"]["STATUS"] == "Deployed" @@ -515,9 +515,9 @@ def test_supabase_enabled_failure(tmp_path): mock_result.returncode = 1 milestone = SupabaseMilestone() with patch.object(milestone, "_get_script_env", return_value={}), \ - patch("installer.core.milestones.subprocess.run", return_value=mock_result), \ - patch("installer.core.env._normalize_cluster_env", return_value="dev"), \ - patch("installer.core.env._parse_bool", return_value=False): + patch("knoe.core.milestones.subprocess.run", return_value=mock_result), \ + patch("knoe.core.env._normalize_cluster_env", return_value="dev"), \ + patch("knoe.core.env._parse_bool", return_value=False): with pytest.raises(Exception, match="[Ss]upabase"): milestone.execute(state) assert state.config_data["Supabase"]["STATUS"] == "Attempted" @@ -537,9 +537,9 @@ def test_supabase_enabled_with_progress(tmp_path): milestone = SupabaseMilestone() progress = MagicMock() with patch.object(milestone, "_get_script_env", return_value={}), \ - patch("installer.core.milestones.subprocess.run", return_value=mock_result), \ - patch("installer.core.env._normalize_cluster_env", return_value="dev"), \ - patch("installer.core.env._parse_bool", return_value=False): + patch("knoe.core.milestones.subprocess.run", return_value=mock_result), \ + patch("knoe.core.env._normalize_cluster_env", return_value="dev"), \ + patch("knoe.core.env._parse_bool", return_value=False): milestone.execute(state, progress=progress) progress.assert_called() @@ -564,8 +564,8 @@ def test_supabase_deploy_skips_prefetch_when_preloaded(tmp_path): milestone = SupabaseMilestone() with patch.object(milestone, "_get_script_env", return_value={}), \ - patch("installer.core.milestones.subprocess.run", side_effect=capture_run), \ - patch("installer.core.env._normalize_cluster_env", return_value="dev"): + patch("knoe.core.milestones.subprocess.run", side_effect=capture_run), \ + patch("knoe.core.env._normalize_cluster_env", return_value="dev"): milestone.execute(state) assert "--skip-prefetch" in captured_cmd @@ -580,7 +580,7 @@ def test_dependencies_auto_install_disabled(): state = _make_state(**{"dependencies.auto_install_missing": "False"}) milestone = DependenciesMilestone() missing_dep = {"id": "brew", "name": "Homebrew", "install_cmd": "brew-install"} - with patch("installer.config.get_dep_info", return_value=(False, None, None)): + with patch("knoe.config.get_dep_info", return_value=(False, None, None)): milestone.execute(state) assert state.config_data.get("Dependencies", {}).get("STATUS") == "Missing" @@ -590,8 +590,8 @@ def test_dependencies_no_install_cmd(): state = _make_state(**{"dependencies.auto_install_missing": "True"}) milestone = DependenciesMilestone() # First call: dep missing; second call (re-verify): still missing - with patch("installer.config.get_dep_info", return_value=(False, None, None)), \ - patch("installer.config.DEPENDENCIES", + with patch("knoe.config.get_dep_info", return_value=(False, None, None)), \ + patch("knoe.config.DEPENDENCIES", [{"id": "mytool", "name": "MyTool"}]): milestone.execute(state) assert state.config_data.get("Dependencies", {}).get("STATUS") == "Missing" @@ -605,8 +605,8 @@ def test_dependencies_install_skipped_by_config(): }) milestone = DependenciesMilestone() dep = {"id": "brew", "name": "Homebrew", "install_cmd": "brew-install"} - with patch("installer.config.get_dep_info", return_value=(False, None, None)), \ - patch("installer.config.DEPENDENCIES", [dep]): + with patch("knoe.config.get_dep_info", return_value=(False, None, None)), \ + patch("knoe.config.DEPENDENCIES", [dep]): milestone.execute(state) assert state.config_data.get("Dependencies", {}).get("STATUS") == "Missing" @@ -616,8 +616,8 @@ def test_dependencies_install_fails(): state = _make_state(**{"dependencies.auto_install_missing": "True"}) milestone = DependenciesMilestone() dep = {"id": "brew", "name": "Homebrew", "install_cmd": "brew-install"} - with patch("installer.config.get_dep_info", return_value=(False, None, None)), \ - patch("installer.config.DEPENDENCIES", [dep]), \ + with patch("knoe.config.get_dep_info", return_value=(False, None, None)), \ + patch("knoe.config.DEPENDENCIES", [dep]), \ patch.object(milestone, "_run_cmd", return_value=1): milestone.execute(state) assert state.config_data.get("Dependencies", {}).get("STATUS") == "Missing" @@ -637,8 +637,8 @@ def test_dependencies_install_succeeds_all(): return (False, None, None) return (True, "/usr/bin/brew", "4.0") - with patch("installer.config.get_dep_info", side_effect=_dep_info), \ - patch("installer.config.DEPENDENCIES", [dep]), \ + with patch("knoe.config.get_dep_info", side_effect=_dep_info), \ + patch("knoe.config.DEPENDENCIES", [dep]), \ patch.object(milestone, "_run_cmd", return_value=0): milestone.execute(state) assert state.config_data.get("Dependencies", {}).get("STATUS") == "All installed" @@ -657,7 +657,7 @@ def test_regenerate_port_mapping_cfg_k3d_mode(tmp_path): ), } milestone = InitializationScriptsMilestone() - with patch("installer.core.milestones.inst_config.PROJECT_ROOT", tmp_path): + with patch("knoe.core.milestones.inst_config.PROJECT_ROOT", tmp_path): (tmp_path / "conf").mkdir(exist_ok=True) milestone._regenerate_port_mapping_cfg(state, "k3d") mapping = (tmp_path / "conf" / "port-mapping.cfg").read_text() @@ -673,7 +673,7 @@ def test_regenerate_port_mapping_cfg_k3s_mode(tmp_path): ), } milestone = InitializationScriptsMilestone() - with patch("installer.core.milestones.inst_config.PROJECT_ROOT", tmp_path): + with patch("knoe.core.milestones.inst_config.PROJECT_ROOT", tmp_path): (tmp_path / "conf").mkdir(exist_ok=True) milestone._regenerate_port_mapping_cfg(state, "k3s") mapping = (tmp_path / "conf" / "port-mapping.cfg").read_text() @@ -688,7 +688,7 @@ def test_regenerate_port_mapping_cfg_skips_wrong_prefix(tmp_path): ), } milestone = InitializationScriptsMilestone() - with patch("installer.core.milestones.inst_config.PROJECT_ROOT", tmp_path): + with patch("knoe.core.milestones.inst_config.PROJECT_ROOT", tmp_path): (tmp_path / "conf").mkdir(exist_ok=True) # k3d mode should skip k3s-prefixed entries milestone._regenerate_port_mapping_cfg(state, "k3d") @@ -705,7 +705,7 @@ def test_regenerate_port_mapping_cfg_resolves_namespace(tmp_path): ), } milestone = InitializationScriptsMilestone() - with patch("installer.core.milestones.inst_config.PROJECT_ROOT", tmp_path): + with patch("knoe.core.milestones.inst_config.PROJECT_ROOT", tmp_path): (tmp_path / "conf").mkdir(exist_ok=True) milestone._regenerate_port_mapping_cfg(state, "k3d") mapping = (tmp_path / "conf" / "port-mapping.cfg").read_text() @@ -721,7 +721,7 @@ def test_regenerate_port_mapping_cfg_skips_unresolvable_port(tmp_path): ), } milestone = InitializationScriptsMilestone() - with patch("installer.core.milestones.inst_config.PROJECT_ROOT", tmp_path): + with patch("knoe.core.milestones.inst_config.PROJECT_ROOT", tmp_path): (tmp_path / "conf").mkdir(exist_ok=True) milestone._regenerate_port_mapping_cfg(state, "k3d") mapping = (tmp_path / "conf" / "port-mapping.cfg").read_text() @@ -731,7 +731,7 @@ def test_regenerate_port_mapping_cfg_skips_unresolvable_port(tmp_path): def test_regenerate_port_mapping_cfg_empty_section(tmp_path): state = _make_state() milestone = InitializationScriptsMilestone() - with patch("installer.core.milestones.inst_config.PROJECT_ROOT", tmp_path): + with patch("knoe.core.milestones.inst_config.PROJECT_ROOT", tmp_path): (tmp_path / "conf").mkdir(exist_ok=True) milestone._regenerate_port_mapping_cfg(state, "k3d") mapping = (tmp_path / "conf" / "port-mapping.cfg").read_text() @@ -742,7 +742,7 @@ def test_regenerate_port_mapping_cfg_write_error(tmp_path): """Write failure should be caught and logged, not raised.""" state = _make_state() milestone = InitializationScriptsMilestone() - with patch("installer.core.milestones.inst_config.PROJECT_ROOT", tmp_path), \ + with patch("knoe.core.milestones.inst_config.PROJECT_ROOT", tmp_path), \ patch("pathlib.Path.write_text", side_effect=OSError("disk full")): (tmp_path / "conf").mkdir(exist_ok=True) # Should not raise @@ -766,8 +766,8 @@ def test_supabase_k8s_mode_for_prod(tmp_path): return mock_result milestone = SupabaseMilestone() with patch.object(milestone, "_get_script_env", return_value={}), \ - patch("installer.core.milestones.subprocess.run", side_effect=capture_run), \ - patch("installer.core.env._normalize_cluster_env", return_value="prod"), \ - patch("installer.core.env._parse_bool", return_value=False): + patch("knoe.core.milestones.subprocess.run", side_effect=capture_run), \ + patch("knoe.core.env._normalize_cluster_env", return_value="prod"), \ + patch("knoe.core.env._parse_bool", return_value=False): milestone.execute(state) assert "k8s" in captured_cmd diff --git a/tests/installer/test_monitor.py b/tests/installer/test_monitor.py index 3a3a8ab..5a77546 100644 --- a/tests/installer/test_monitor.py +++ b/tests/installer/test_monitor.py @@ -12,7 +12,7 @@ from pathlib import Path from unittest.mock import MagicMock, patch, call import pytest -from installer.core.monitor import ( +from knoe.core.monitor import ( _parse_pf_cfg, write_port_forwards_cfg, check_port_conflicts, @@ -275,7 +275,7 @@ def test_start_port_forwards_empty(): def test_start_port_forwards_success(capsys): mapping = {"id": "argocd", "ns": "argocd", "svc": "argocd-server", "local": "8080", "remote": "80", "address": "127.0.0.1"} mock_proc = MagicMock(spec=subprocess.Popen) - with patch("installer.core.monitor.subprocess.Popen", return_value=mock_proc) as mock_popen: + with patch("knoe.core.monitor.subprocess.Popen", return_value=mock_proc) as mock_popen: result = _start_port_forwards([mapping]) assert len(result) == 1 m_out, proc_out = result[0] @@ -288,7 +288,7 @@ def test_start_port_forwards_success(capsys): def test_start_port_forwards_failure(capsys): mapping = {"id": "broken", "ns": "default", "svc": "nosvc", "local": "9999", "remote": "9999", "address": "127.0.0.1"} - with patch("installer.core.monitor.subprocess.Popen", side_effect=OSError("not found")): + with patch("knoe.core.monitor.subprocess.Popen", side_effect=OSError("not found")): result = _start_port_forwards([mapping]) assert len(result) == 1 _, proc = result[0] @@ -301,7 +301,7 @@ def test_start_port_forwards_failure(capsys): def test_start_port_forwards_verbose(capsys): mapping = {"id": "test", "ns": "default", "svc": "myapp", "local": "8000", "remote": "8000", "address": "127.0.0.1"} mock_proc = MagicMock(spec=subprocess.Popen) - with patch("installer.core.monitor.subprocess.Popen", return_value=mock_proc): + with patch("knoe.core.monitor.subprocess.Popen", return_value=mock_proc): _start_port_forwards([mapping], verbose=True) out = capsys.readouterr().out assert "[PORT-FWD]" in out @@ -313,7 +313,7 @@ def test_start_port_forwards_multiple(capsys): {"id": "b", "ns": "default", "svc": "svc-b", "local": "8002", "remote": "80", "address": "127.0.0.1"}, ] mock_proc = MagicMock(spec=subprocess.Popen) - with patch("installer.core.monitor.subprocess.Popen", return_value=mock_proc): + with patch("knoe.core.monitor.subprocess.Popen", return_value=mock_proc): result = _start_port_forwards(mappings) assert len(result) == 2 assert result[0][0]["id"] == "a" diff --git a/tests/installer/test_namespace_propagation.py b/tests/installer/test_namespace_propagation.py index d2fc7c5..ede0fe7 100644 --- a/tests/installer/test_namespace_propagation.py +++ b/tests/installer/test_namespace_propagation.py @@ -5,7 +5,7 @@ from unittest.mock import MagicMock import pytest -from installer.ui.screens.cfg import ConfigMixin +from knoe.ui.screens.cfg import ConfigMixin class _DummyNamespaceApp(ConfigMixin): diff --git a/tests/installer/test_ollama_scan.py b/tests/installer/test_ollama_scan.py index 87f0326..9b8557f 100644 --- a/tests/installer/test_ollama_scan.py +++ b/tests/installer/test_ollama_scan.py @@ -1,9 +1,9 @@ -"""Tests for `installer.core.ollama_scan` parsing helpers.""" +"""Tests for `knoe.core.ollama_scan` parsing helpers.""" from __future__ import annotations -from installer.core.ollama_scan import parse_ollama_scan_stdout +from knoe.core.ollama_scan import parse_ollama_scan_stdout def test_parse_ollama_scan_stdout_parses_host_port_models(): diff --git a/tests/installer/test_policy.py b/tests/installer/test_policy.py index a568388..395c2bf 100644 --- a/tests/installer/test_policy.py +++ b/tests/installer/test_policy.py @@ -9,7 +9,7 @@ def _kubectl_nodes_json(*, items: list[dict]) -> str: def test_evaluate_optional_workloads_allowed_true_two_ready(monkeypatch): - from installer.core import policy + from knoe.core import policy class _Res: returncode = 0 @@ -50,7 +50,7 @@ def test_evaluate_optional_workloads_allowed_true_two_ready(monkeypatch): def test_evaluate_optional_workloads_allowed_false_one_ready(monkeypatch): - from installer.core import policy + from knoe.core import policy class _Res: returncode = 0 @@ -80,7 +80,7 @@ def test_evaluate_optional_workloads_allowed_false_one_ready(monkeypatch): def test_evaluate_optional_workloads_ignores_unschedulable(monkeypatch): - from installer.core import policy + from knoe.core import policy class _Res: returncode = 0 @@ -116,7 +116,7 @@ def test_evaluate_optional_workloads_ignores_unschedulable(monkeypatch): def test_evaluate_optional_workloads_kubectl_error_is_disallowed(monkeypatch): - from installer.core import policy + from knoe.core import policy class _Res: returncode = 1 diff --git a/tests/installer/test_prole_conf.py b/tests/installer/test_prole_conf.py index 426c60e..beead21 100644 --- a/tests/installer/test_prole_conf.py +++ b/tests/installer/test_prole_conf.py @@ -3,7 +3,7 @@ from pathlib import Path import pytest -from installer import prole_conf +from knoe import prole_conf def _symlink_supported(tmp_path: Path) -> bool: diff --git a/tests/installer/test_push_docker_image_ca_fallback.py b/tests/installer/test_push_docker_image_ca_fallback.py index 34dff2b..1d2620c 100644 --- a/tests/installer/test_push_docker_image_ca_fallback.py +++ b/tests/installer/test_push_docker_image_ca_fallback.py @@ -6,7 +6,7 @@ from types import SimpleNamespace def test_push_docker_image_prefers_tls_verified_skopeo_when_ca_provided( tmp_path, monkeypatch ): - from installer.core import env + from knoe.core import env ca = tmp_path / "registry-ca.pem" ca.write_text("dummy-ca") @@ -31,7 +31,7 @@ def test_push_docker_image_prefers_tls_verified_skopeo_when_ca_provided( monkeypatch.setattr(env.subprocess, "run", fake_run) - ok = env._push_docker_image("myrddin.prole.org:5000/prole-db:18-118") + ok = env._push_docker_image("myrddin.prole.org:5000/knoe-db:18-118") assert ok is True assert len(calls) == 2 @@ -39,7 +39,7 @@ def test_push_docker_image_prefers_tls_verified_skopeo_when_ca_provided( def test_push_docker_image_auto_detects_repo_cert_for_myrddin(monkeypatch): """Regression: when no env var is provided, prefer repo-shipped certs (Ansible prole_ssl role).""" - from installer.core import env + from knoe.core import env monkeypatch.delenv("PROLE_REGISTRY_CA_CERT", raising=False) monkeypatch.delenv("REGISTRY_CA_CERT", raising=False) @@ -69,6 +69,6 @@ def test_push_docker_image_auto_detects_repo_cert_for_myrddin(monkeypatch): monkeypatch.setattr(env.subprocess, "run", fake_run) - ok = env._push_docker_image("myrddin.prole.org:5000/prole-db:18-118") + ok = env._push_docker_image("myrddin.prole.org:5000/knoe-db:18-118") assert ok is True assert len(calls) == 2 diff --git a/tests/installer/test_registry_k3s_selection.py b/tests/installer/test_registry_k3s_selection.py index 8d27ba5..8fdf96f 100644 --- a/tests/installer/test_registry_k3s_selection.py +++ b/tests/installer/test_registry_k3s_selection.py @@ -7,8 +7,8 @@ from pathlib import Path import pytest -from installer.ui.screens.docker import DockerScreenMixin -import installer.ui.screens.docker as docker_mod +from knoe.ui.screens.docker import DockerScreenMixin +import knoe.ui.screens.docker as docker_mod class _DummyDockerScreen(DockerScreenMixin): diff --git a/tests/installer/test_registry_rescan_after_init.py b/tests/installer/test_registry_rescan_after_init.py index 2939c4b..cb28cb5 100644 --- a/tests/installer/test_registry_rescan_after_init.py +++ b/tests/installer/test_registry_rescan_after_init.py @@ -4,7 +4,7 @@ from unittest.mock import MagicMock def test_db_rescan_registry_status_after_init_schedules_multiple_checks(): - from installer.ui.screens.database import DatabaseScreenMixin + from knoe.ui.screens.database import DatabaseScreenMixin class _Dummy(DatabaseScreenMixin): def __init__(self): diff --git a/tests/installer/test_screen.py b/tests/installer/test_screen.py index 0e73bde..1007877 100644 --- a/tests/installer/test_screen.py +++ b/tests/installer/test_screen.py @@ -1,7 +1,7 @@ import pytest from unittest.mock import MagicMock, patch import tkinter as tk -from installer.screen import render_title, render_paragraph, TerminalConsole +from knoe.screen import render_title, render_paragraph, TerminalConsole @pytest.fixture diff --git a/tests/installer/test_services_init_scripts.py b/tests/installer/test_services_init_scripts.py index af84917..3a3b27a 100644 --- a/tests/installer/test_services_init_scripts.py +++ b/tests/installer/test_services_init_scripts.py @@ -5,8 +5,8 @@ from unittest.mock import MagicMock def test_tk_init_scripts_runner_invokes_init_certmgr(tmp_path, monkeypatch): - import installer.ui.screens.services as services - from installer.ui.screens.services import ServicesScreenMixin + import knoe.ui.screens.services as services + from knoe.ui.screens.services import ServicesScreenMixin class _Var: def __init__(self, value=""): @@ -137,8 +137,8 @@ def test_init_monitoring_grafana_password_capture_does_not_wipe_monitoring_data_ ): """Regression: capturing Grafana admin password must not wipe other Monitoring values.""" - import installer.ui.screens.services as services - from installer.ui.screens.services import ServicesScreenMixin + import knoe.ui.screens.services as services + from knoe.ui.screens.services import ServicesScreenMixin class _Var: def __init__(self, value=""): diff --git a/tests/installer/test_stream_exec.py b/tests/installer/test_stream_exec.py index 2846c0a..6f62546 100644 --- a/tests/installer/test_stream_exec.py +++ b/tests/installer/test_stream_exec.py @@ -1,7 +1,7 @@ import sys import time -from installer.core.stream_exec import run_streaming_cmd +from knoe.core.stream_exec import run_streaming_cmd def test_run_streaming_cmd_streams_stdout_incrementally(): diff --git a/tests/installer/test_verify_k3s_services_status.py b/tests/installer/test_verify_k3s_services_status.py index d71c49a..6a7e9d3 100644 --- a/tests/installer/test_verify_k3s_services_status.py +++ b/tests/installer/test_verify_k3s_services_status.py @@ -10,7 +10,7 @@ if str(PROJECT_ROOT) not in sys.path: sys.path.insert(0, str(PROJECT_ROOT)) -from installer.core import env as core_env +from knoe.core import env as core_env def test_verify_k3s_services_status_does_not_require_token_when_kubeconfig_available( diff --git a/tests/ops/test_components.py b/tests/ops/test_components.py new file mode 100644 index 0000000..ed18d71 --- /dev/null +++ b/tests/ops/test_components.py @@ -0,0 +1,17 @@ +from __future__ import annotations + +from pathlib import Path +from unittest.mock import patch + +from knoe.ops.context import KnoeContext +from knoe.ops.components import argocd + + +def test_component_wrapper_dispatches_to_expected_script(tmp_path: Path): + ctx = KnoeContext(project_root=tmp_path, env={}) + + with patch("knoe.ops.components.argocd.run_script", return_value=0) as m: + rc = argocd.initialize(ctx) + + assert rc == 0 + m.assert_called_once_with(ctx, "init_argocd.sh", "initialize") diff --git a/tests/ops/test_context.py b/tests/ops/test_context.py new file mode 100644 index 0000000..5d3ce30 --- /dev/null +++ b/tests/ops/test_context.py @@ -0,0 +1,24 @@ +from __future__ import annotations + +from pathlib import Path + +from knoe.ops.context import KnoeContext + + +def test_knoe_context_construction(tmp_path: Path): + ctx = KnoeContext( + project_root=tmp_path, + cfg_path=None, + mode="dev", + namespace="ns", + service_namespace="svc", + env={"HELLO": "world"}, + logger=lambda _: None, + err_logger=lambda _: None, + ) + assert ctx.project_root == tmp_path + assert ctx.cfg_path is None + assert ctx.mode == "dev" + assert ctx.namespace == "ns" + assert ctx.service_namespace == "svc" + assert ctx.env["HELLO"] == "world" diff --git a/tests/ops/test_files.py b/tests/ops/test_files.py new file mode 100644 index 0000000..b05f85e --- /dev/null +++ b/tests/ops/test_files.py @@ -0,0 +1,19 @@ +from __future__ import annotations + +from pathlib import Path + +from knoe.ops.files import write_if_changed + + +def test_write_if_changed(tmp_path: Path): + p = tmp_path / "a.txt" + + assert write_if_changed(p, "hello\n") is True + assert p.read_text(encoding="utf-8") == "hello\n" + + # Unchanged + assert write_if_changed(p, "hello\n") is False + + # Changed + assert write_if_changed(p, "hello2\n") is True + assert p.read_text(encoding="utf-8") == "hello2\n" diff --git a/tests/ops/test_k8s.py b/tests/ops/test_k8s.py new file mode 100644 index 0000000..0d31eb8 --- /dev/null +++ b/tests/ops/test_k8s.py @@ -0,0 +1,34 @@ +from __future__ import annotations + +import subprocess +from pathlib import Path +from unittest.mock import patch + +from knoe.ops.context import KnoeContext +from knoe.ops.k8s import apply_yaml, kubectl + + +def test_kubectl_command_assembly(tmp_path: Path): + ctx = KnoeContext(project_root=tmp_path, env={"FOO": "BAR"}) + cp = subprocess.CompletedProcess(args=["kubectl"], returncode=0, stdout="ok\n", stderr="") + + with patch("knoe.ops.k8s.check", return_value=cp) as m: + out = kubectl(ctx, "get", "pods", namespace="myns") + + assert out == "ok\n" + (cmd,), kwargs = m.call_args + assert cmd[:3] == ["kubectl", "get", "pods"] + assert cmd[-2:] == ["-n", "myns"] + assert kwargs["env"]["FOO"] == "BAR" + + +def test_apply_yaml_passes_stdin(tmp_path: Path): + ctx = KnoeContext(project_root=tmp_path, env={}) + cp = subprocess.CompletedProcess(args=["kubectl"], returncode=0, stdout="", stderr="") + + with patch("knoe.ops.k8s.check", return_value=cp) as m: + apply_yaml(ctx, "apiVersion: v1\nkind: Namespace\nmetadata:\n name: x\n") + + (cmd,), kwargs = m.call_args + assert cmd[:4] == ["kubectl", "apply", "-f", "-"] + assert "apiVersion" in (kwargs.get("input_text") or "") diff --git a/tests/ops/test_legacy_shell.py b/tests/ops/test_legacy_shell.py new file mode 100644 index 0000000..fabfc5c --- /dev/null +++ b/tests/ops/test_legacy_shell.py @@ -0,0 +1,36 @@ +from __future__ import annotations + +from pathlib import Path +from unittest.mock import patch + +from knoe.ops.context import KnoeContext +from knoe.ops.legacy_shell import run_script + + +def test_run_script_argument_assembly(tmp_path: Path): + etc_dir = tmp_path / "etc" + etc_dir.mkdir(parents=True, exist_ok=True) + (etc_dir / "init_argocd.sh").write_text("#!/usr/bin/env bash\necho ok\n", encoding="utf-8") + + ctx = KnoeContext( + project_root=tmp_path, + cfg_path=tmp_path / "conf" / "prole.cfg", + mode="m", + namespace="ns", + service_namespace="svcns", + env={"X": "1"}, + ) + + with patch("knoe.ops.legacy_shell.run_streaming", return_value=0) as m: + rc = run_script(ctx, "init_argocd.sh", "status", extra_args=["--foo", "bar"]) + + assert rc == 0 + (cmd,), kwargs = m.call_args + assert cmd[0] == "bash" + assert cmd[2:] == ["status", "--foo", "bar"] + assert kwargs["cwd"] == tmp_path + assert kwargs["env"]["X"] == "1" + assert kwargs["env"]["PROLE_MODE"] == "m" + assert kwargs["env"]["PROLE_NAMESPACE"] == "ns" + assert kwargs["env"]["PROLE_SERVICE_NAMESPACE"] == "svcns" + assert kwargs["env"]["PROLE_CFG"].endswith("conf/prole.cfg") diff --git a/tests/run_tests.sh b/tests/run_tests.sh index 45ea7bf..500e688 100755 --- a/tests/run_tests.sh +++ b/tests/run_tests.sh @@ -6,7 +6,7 @@ export PYTHONPATH=$PYTHONPATH:. # Check if pytest-cov is installed if pytest --trace-config | grep -q "pytest_cov"; then echo "Running tests with coverage..." - pytest --cov=installer --cov=install --cov-report=term-missing --cov-report=html tests/ + pytest --cov=knoe --cov=install --cov-report=term-missing --cov-report=html tests/ RET=$? echo "Coverage report (HTML) generated in htmlcov/index.html" exit $RET diff --git a/tests/test_all_prole_home_fixes.sh b/tests/test_all_prole_home_fixes.sh index b422862..82736bb 100755 --- a/tests/test_all_prole_home_fixes.sh +++ b/tests/test_all_prole_home_fixes.sh @@ -42,16 +42,17 @@ test_item "Scan directory writable" python3 -c "from pathlib import Path; f = Pa echo "" echo "CODE VERIFICATION" -test_item "Build uses .prole/build" grep -q "prole_home / \"build\"" install.py -test_item "Scan uses .prole/scan" grep -q "prole_home / \"scan\"" install.py -test_item "Docker build copies context" grep -q "shutil.copytree(source_dir, build_dir)" install.py -test_item "Scan runs with cwd" grep -q "cwd=str(scan_dir)" install.py +test_item "Build uses .prole/build" grep -q "prole_home / \"build\"" knoe/core/actions.py +test_item "Scan uses .prole/scan" grep -q "prole_home / \"scan\"" knoe/core/actions.py +test_item "Docker build uses copy_build_context_dir (core)" grep -q "copy_build_context_dir(source_dir, build_dir)" knoe/core/actions.py +test_item "Docker build uses copy_build_context_dir (UI)" grep -q "copy_build_context_dir(source_dir, build_dir)" knoe/ui/screens/database.py +test_item "Scan runs with cwd" grep -q "cwd=str(scan_dir)" knoe/core/actions.py echo "" echo "SPEC FILE VERIFICATION" python3 scripts/generate_spec.py > /dev/null 2>&1 -test_item "Spec includes prole-db" grep -q "prole-db" installer.spec -test_item "Spec includes prole-agent" grep -q "prole-agent" installer.spec +test_item "Spec includes prole-db" grep -q "prole-db" knoe.spec +test_item "Spec includes prole-agent" grep -q "prole-agent" knoe.spec # Cleanup echo "" diff --git a/tests/test_build_context.py b/tests/test_build_context.py new file mode 100644 index 0000000..26c7c8c --- /dev/null +++ b/tests/test_build_context.py @@ -0,0 +1,41 @@ +import os +from pathlib import Path + +from knoe.core.build_context import copy_build_context_dir + + +def test_copy_build_context_dir_ignores_data_and_keeps_dangling_symlinks(tmp_path: Path): + source_dir = tmp_path / "source" + build_dir = tmp_path / "build" + source_dir.mkdir() + + (source_dir / "Dockerfile").write_text("FROM scratch\n") + (source_dir / "keep.txt").write_text("ok") + + # Create a runtime-like `data/` tree that may contain problematic entries. + pgdata = source_dir / "data" / "pvc-123" / "pgdata" + pgdata.mkdir(parents=True) + if hasattr(os, "symlink"): + os.symlink("does_not_exist", pgdata / "pg_wal") + + # Also ensure we can copy dangling symlinks outside ignored directories. + if hasattr(os, "symlink"): + os.symlink("missing_target", source_dir / "dangling_link") + + # Pre-create build_dir with stale content to ensure it gets replaced. + build_dir.mkdir() + (build_dir / "stale.txt").write_text("stale") + + copy_build_context_dir(source_dir, build_dir) + + assert (build_dir / "Dockerfile").exists() + assert (build_dir / "keep.txt").read_text() == "ok" + assert not (build_dir / "stale.txt").exists() + + # `data/` should never be part of the Docker build context. + assert not (build_dir / "data").exists() + + if hasattr(os, "symlink"): + link = build_dir / "dangling_link" + assert link.is_symlink() + assert os.readlink(link) == "missing_target" diff --git a/tests/test_build_system.sh b/tests/test_build_system.sh index 0f533e6..e498979 100755 --- a/tests/test_build_system.sh +++ b/tests/test_build_system.sh @@ -14,8 +14,8 @@ test -f img/proleIcon.png && echo " ✓ Icon file exists" echo "" echo "3. Testing Python modules..." python3 -c "from install import ProleController; ProleController('.')" && echo " ✓ ProleController works" -python3 -c "from installer.ncurses_installer import run_ncurses_installer" && echo " ✓ Ncurses installer imports" -python3 -c "from installer.ncurses_ui import CursesWindow" && echo " ✓ Ncurses UI imports" +python3 -c "from knoe.ncurses_installer import run_ncurses_installer" && echo " ✓ Ncurses installer imports" +python3 -c "from knoe.ncurses_ui import CursesWindow" && echo " ✓ Ncurses UI imports" echo "" echo "4. Testing command-line interface..." diff --git a/tests/test_cluster_screen_observe_only.py b/tests/test_cluster_screen_observe_only.py index c8646ca..91e9529 100644 --- a/tests/test_cluster_screen_observe_only.py +++ b/tests/test_cluster_screen_observe_only.py @@ -4,7 +4,7 @@ from __future__ import annotations import json -from installer.ui.screens.cluster import ClusterScreenMixin +from knoe.ui.screens.cluster import ClusterScreenMixin class _Var: @@ -46,7 +46,7 @@ def test_cluster_status_refresh_is_observe_only(monkeypatch): self._target() monkeypatch.setattr( - "installer.ui.screens.cluster.threading.Thread", _ImmediateThread, raising=True + "knoe.ui.screens.cluster.threading.Thread", _ImmediateThread, raising=True ) class Dummy(ClusterScreenMixin): @@ -131,11 +131,11 @@ def test_service_namespace_infers_registry_namespace(monkeypatch): return _Res() monkeypatch.setattr( - "installer.ui.screens.cluster.subprocess.run", fake_run, raising=True + "knoe.ui.screens.cluster.subprocess.run", fake_run, raising=True ) # Avoid reliance on an actual kubeconfig in test environment. monkeypatch.setattr( - "installer.ui.screens.cluster._find_kubeconfig_file", lambda *_a, **_k: "", raising=True + "knoe.ui.screens.cluster._find_kubeconfig_file", lambda *_a, **_k: "", raising=True ) assert Dummy()._get_service_namespace() == "knoe-system" diff --git a/tests/test_database_options.py b/tests/test_database_options.py index 517c2b2..793ea15 100644 --- a/tests/test_database_options.py +++ b/tests/test_database_options.py @@ -35,21 +35,21 @@ def mock_installer(tmp_path): # We need to patch the tk references in the modules that use them, # because they might have already been imported with a different mock. patches = [ - patch("installer.ui.screens.PROJECT_ROOT", tmp_path), - patch("installer.ui.screens.database_options.PROJECT_ROOT", tmp_path), - patch("installer.core.controller.ProleController.run_script"), + patch("knoe.ui.screens.PROJECT_ROOT", tmp_path), + patch("knoe.ui.screens.database_options.PROJECT_ROOT", tmp_path), + patch("knoe.core.controller.ProleController.run_script"), patch.object(ProleInstaller, "_load_database_versions"), - patch("installer.ui.screens.database_options.tk.BooleanVar", side_effect=lambda value=None: MockVar(value)), - patch("installer.ui.screens.database_options.tk.StringVar", side_effect=lambda value=None: MockVar(value)), - patch("installer.ui.screens.database_options.tk.ttk.Treeview"), - patch("installer.ui.screens.database_options.tk.ttk.Scrollbar"), - patch("installer.ui.screens.database_options.tk.ttk.Combobox"), - patch("installer.ui.screens.database_options.tk.Radiobutton"), - patch("installer.ui.screens.database_options.tk.Checkbutton"), - patch("installer.ui.screens.database_options.tk.Label"), + patch("knoe.ui.screens.database_options.tk.BooleanVar", side_effect=lambda value=None: MockVar(value)), + patch("knoe.ui.screens.database_options.tk.StringVar", side_effect=lambda value=None: MockVar(value)), + patch("knoe.ui.screens.database_options.tk.ttk.Treeview"), + patch("knoe.ui.screens.database_options.tk.ttk.Scrollbar"), + patch("knoe.ui.screens.database_options.tk.ttk.Combobox"), + patch("knoe.ui.screens.database_options.tk.Radiobutton"), + patch("knoe.ui.screens.database_options.tk.Checkbutton"), + patch("knoe.ui.screens.database_options.tk.Label"), # Also for the main class if it uses them - patch("installer.ui.screens.tk.BooleanVar", side_effect=lambda value=None: MockVar(value)), - patch("installer.ui.screens.tk.StringVar", side_effect=lambda value=None: MockVar(value)), + patch("knoe.ui.screens.tk.BooleanVar", side_effect=lambda value=None: MockVar(value)), + patch("knoe.ui.screens.tk.StringVar", side_effect=lambda value=None: MockVar(value)), ] with ExitStack() as stack: @@ -87,7 +87,7 @@ def test_encryption_toggle(mock_installer): # Actually it might be easier to just test the logic if I can access it. # Let's mock the render to get the callback - with patch("installer.ui.screens.database_options.tk.Checkbutton") as mock_cb: + with patch("knoe.ui.screens.database_options.tk.Checkbutton") as mock_cb: mock_installer._render_database_options_page() args, kwargs = mock_cb.call_args on_encryption_toggle = kwargs["command"] diff --git a/tests/test_docker_build_fix.sh b/tests/test_docker_build_fix.sh index c9e2f0d..90ba6ad 100755 --- a/tests/test_docker_build_fix.sh +++ b/tests/test_docker_build_fix.sh @@ -17,7 +17,7 @@ fi # Test 2: Spec includes prole-db echo "2. Testing spec file includes prole-db..." python3 scripts/generate_spec.py > /dev/null 2>&1 -if grep -q "('prole-db', 'prole-db')" installer.spec; then +if grep -q "('prole-db', 'prole-db')" knoe.spec; then echo " ✓ prole-db included in spec" else echo " ✗ prole-db not in spec" @@ -30,7 +30,7 @@ python3 << 'PYTEST' import sys from pathlib import Path sys.path.insert(0, str(Path.cwd())) -from install import get_resource_path +from knoe.core.env import get_resource_path prole_db = get_resource_path("prole-db") if prole_db.exists() and (prole_db / "Dockerfile").exists(): @@ -68,25 +68,29 @@ echo "5. Testing build context copy..." python3 << 'PYTEST' import sys from pathlib import Path -import shutil sys.path.insert(0, str(Path.cwd())) -from install import get_resource_path +from knoe.core.env import get_resource_path +from knoe.core.build_context import copy_build_context_dir prole_home = Path.home() / ".prole" build_dir = prole_home / "build" / "prole-db-test" source_dir = get_resource_path("prole-db") try: - if build_dir.exists(): - shutil.rmtree(build_dir) - shutil.copytree(source_dir, build_dir) + copy_build_context_dir(source_dir, build_dir) # Verify if (build_dir / "Dockerfile").exists(): print(f" ✓ Copied build context successfully") # Cleanup - shutil.rmtree(prole_home / "build" / "prole-db-test") + # Cleanup + try: + import shutil + + shutil.rmtree(prole_home / "build" / "prole-db-test") + except Exception: + pass else: print(" ✗ Copy incomplete") sys.exit(1) @@ -106,5 +110,5 @@ echo " • From source: Uses PROJECT_ROOT/prole-db" echo " • From package: Copies to ~/.prole/build/prole-db" echo "" echo "Build directory: ~/.prole/build/prole-db" -echo "Build command: docker build -t prole-db:TAG ." +echo "Build command: docker build -t knoe-db:TAG ." echo "" diff --git a/tests/test_install_logic.py b/tests/test_install_logic.py index 2fd0875..7802312 100644 --- a/tests/test_install_logic.py +++ b/tests/test_install_logic.py @@ -28,6 +28,9 @@ def installer(tmp_path): # We need to mock ProleInstaller.__init__ because it creates GUI elements with patch.object(ProleInstaller, "__init__", return_value=None): ins = ProleInstaller() + # Core helpers expect `inputs` to exist; bypassing `__init__` means + # we must seed minimal state for method calls. + ins.inputs = {} return ins diff --git a/tests/test_k3s_common_core_namespace_and_repair.py b/tests/test_k3s_common_core_namespace_and_repair.py index ef2bb7b..2bbc7a7 100644 --- a/tests/test_k3s_common_core_namespace_and_repair.py +++ b/tests/test_k3s_common_core_namespace_and_repair.py @@ -2,8 +2,8 @@ from __future__ import annotations from pathlib import Path -from installer.core.actions import ProleConsoleInstaller -from installer.core.controller import ProleController +from knoe.core.actions import ProleConsoleInstaller +from knoe.core.controller import ProleController def test_silent_installer_service_namespace_defaults_to_knoe_system_for_k3s(tmp_path, monkeypatch): @@ -33,13 +33,13 @@ def test_repair_pipeline_has_k3s_dedupe_guard(): assert "knoe-system" not in text # Wrapper should delegate to Python orchestration. - assert "installer.core.repair_pipeline_cli" in text + assert "knoe.core.repair_pipeline_cli" in text # Legacy bash orchestration should no longer live here. assert "dedupe_common_core_default_namespace" not in text # Dedupe logic is now in Python. - py = Path("installer/core/actions.py").read_text(encoding="utf-8") + py = Path("knoe/core/actions.py").read_text(encoding="utf-8") assert "Legacy namespace dedupe" in py assert "prole-svc-kong" in py diff --git a/tests/test_reconcile_blocked_cluster_state.py b/tests/test_reconcile_blocked_cluster_state.py index e25e1dd..b2e874d 100644 --- a/tests/test_reconcile_blocked_cluster_state.py +++ b/tests/test_reconcile_blocked_cluster_state.py @@ -4,9 +4,9 @@ from types import SimpleNamespace import pytest -import installer.core.actions as actions -from installer.core.actions import ProleConsoleInstaller -from installer.core.controller import ProleController +import knoe.core.actions as actions +from knoe.core.actions import ProleConsoleInstaller +from knoe.core.controller import ProleController def _mk_installer(tmp_path) -> ProleConsoleInstaller: diff --git a/tests/test_repair_stale_released_pv_claimref.py b/tests/test_repair_stale_released_pv_claimref.py index 074e273..e85962e 100644 --- a/tests/test_repair_stale_released_pv_claimref.py +++ b/tests/test_repair_stale_released_pv_claimref.py @@ -1,12 +1,13 @@ from __future__ import annotations from contextlib import contextmanager +import json import os from types import SimpleNamespace -import installer.core.actions as actions -from installer.core.actions import ProleConsoleInstaller -from installer.core.controller import ProleController +import knoe.core.actions as actions +from knoe.core.actions import ProleConsoleInstaller +from knoe.core.controller import ProleController @contextmanager @@ -206,13 +207,28 @@ def test_reset_reclaim_stale_released_pvs_patches_concrete_garage_and_openbao( }, ] - installer._list_pending_pvcs = lambda _env, namespaces=None: pending_pvcs # type: ignore[method-assign] - installer._list_released_pvs = lambda _env, storage_class=None: released_pvs # type: ignore[method-assign] + actions._list_pending_pvcs = lambda _env, namespaces=None: pending_pvcs # type: ignore[method-assign] + actions._list_released_pvs = lambda _env, storage_class=None: released_pvs # type: ignore[method-assign] calls: list[list[str]] = [] def fake_run(cmd, **kwargs): - calls.append(list(cmd)) + cmd = list(cmd) + calls.append(cmd) + + if cmd[:4] == ["kubectl", "get", "pvc", "-A"] and "-o" in cmd and "json" in cmd: + return SimpleNamespace( + returncode=0, + stdout=json.dumps({"items": pending_pvcs}), + stderr="", + ) + if cmd[:3] == ["kubectl", "get", "pv"] and "-o" in cmd and "json" in cmd: + return SimpleNamespace( + returncode=0, + stdout=json.dumps({"items": released_pvs}), + stderr="", + ) + return SimpleNamespace(returncode=0, stdout="ok", stderr="") monkeypatch.setattr(actions.subprocess, "run", fake_run)