Refine installer orchestration and Supabase rendering paths

- improve installer/action/controller flow and shell-variable expansion handling across screens\n- adjust Supabase Helm rendering and storage deployment templates\n- align monitoring, cloudnative-pg and repair pipeline behavior with updated config paths\n- refresh and expand installer/core regression tests around milestones, navigation and repair logic

Co-authored-by: Junie <junie@jetbrains.com>
This commit is contained in:
chrisfu 2026-03-31 00:20:34 -07:00
parent c65d336d1f
commit c0a7d0c5dc
53 changed files with 795 additions and 473 deletions

View File

@ -1,5 +1,5 @@
; Prole Master Configuration File
; Generated by install.py on 2026-03-27 21:57:14
; Generated by install.py on 2026-03-31 00:08:32
; This file is used as input for Ansible deployment and k8s cluster creation.
[User]
@ -13,10 +13,12 @@ build.run_build = false
database_options.distribution = percona
database_options.ext.dblink = true
database_options.ext.pg_buffercache = true
database_options.ext.pg_cron = true
database_options.ext.pg_freespacemap = true
database_options.ext.pg_repack = true
database_options.ext.pg_stat_monitor = true
database_options.ext.pg_stat_statements = true
database_options.ext.pg_tde = true
database_options.ext.pgaudit = true
database_options.ext.pgbadger = true
database_options.ext.pgcrypto = true
@ -51,7 +53,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:WmcrTDHDhUyzeCC4:DvGQhp_kiVfcbpTCDncA_t-xH4ktjvCka7jst-7LjWLb-k0NicaNogLRwFT8b5_pJsPnBFnNhcrz-2iD4Zb0-L3i0gILy8LIwwJajRaic9qDNd01Wm_9kRjv_z2XRhFWiwGEm7m3sd__6BYkeKhBXE9lzii8WpGJgo9bAA==}
init_cluster.k3s_token = ${PROLE_SECRET:v1:h4uThQOAj6ASFIFf:_cS5t3-Z3q90-OWiMsX5BnLQnjC8tbomoyuv_UfnNM6XmI2H6yofzJWSyhgNfgFaWxL8ImIwYWVKf9OjvwEhxr54LVSQ9EEm3KQsRW1cADHWnkjl7AGifsi2Wa9ve6E0PueVPAyHC5cIUXJ3Njt_bdegibhjZv6_-jFkGg==}
init_cluster.kerberos_enabled = true
init_cluster.mode = k8s
init_cluster.start_cluster = true
@ -62,14 +64,14 @@ init_db_build.run_build = true
init_password.cluster_name = ${CLUSTER_NAME}
init_password.db_host_port = 5432
init_password.db_namespace = ${DATABASE_NAMESPACE}
init_password.db_password = ${PROLE_SECRET:v1:eN8L5Bo1KE2xGU-Z:y4ybETpw1POYjX2tP96lWTajMLFsiTXn5DNj7s_l6gBZInnGB5MjuxQTZ-G_FBnK}
init_password.db_password_confirm = ${PROLE_SECRET:v1:eN8L5Bo1KE2xGU-Z:y4ybETpw1POYjX2tP96lWTajMLFsiTXn5DNj7s_l6gBZInnGB5MjuxQTZ-G_FBnK}
init_password.db_password = ${PROLE_SECRET:v1:uLCCn7fAxP1AHcv7:zahrhbz50ngH59bVmJUvbCm5OTM1Id_ksd5D61I-GixJETQVMewsr1oC7Avu8m6E}
init_password.db_password_confirm = ${PROLE_SECRET:v1:uLCCn7fAxP1AHcv7:zahrhbz50ngH59bVmJUvbCm5OTM1Id_ksd5D61I-GixJETQVMewsr1oC7Avu8m6E}
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 = ${PROLE_SECRET:v1:1UPXXGF4GW7MIfhR:_hFj8SAVHRd60qnOw-q5MaM9W8CaxNHgPkFnyXmNXXNzCj5x}
kerberos_config.password = ${PROLE_SECRET:v1:O-RQRUsyZ-C-oV7k:-9LlOE5KxpT8qGApgzzH2IneU76fYbCk-GwTsAII1tzxK_P3}
kerberos_config.realm = PROLE.ORG
kerberos_config.test_connection = false
kerberos_config.user = administrator
@ -77,25 +79,28 @@ network_scan.run = true
ollama_config.model =
ollama_config.server_host = fairyland.prole.org
ollama_config.server_port = 11434
supabase_config.pv_base_dir = /synology/d005
supabase_config.pv_node = gandalf.prole.org
[Global]
; Variables used by name in more than one place or assumed global scope
ARGOCD_NAMESPACE = argocd
CLUSTER_ENV = prod
CLUSTER_NAME = knoe-db
DATABASE_NAMESPACE = knoe-db-17-7-043-18-140-17-7-043-18-140
DATABASE_NAMESPACE = knoe-db-17-7-043-18-140-17-7-043-18-140-17-7-043-18-140
DB_HOST_PORT = 5432
DB_PASSWORD = ${PROLE_SECRET:v1:hk7ryF9uC0HMNmE0:ZFtwyX85sqA61qZDuSLEL1gWwuC20r6qCKnTihsGvJ7PODH5XcIRSQ5Ms3Il8lMt}
DB_PASSWORD = ${PROLE_SECRET:v1:7FWWyYsJ27QWE7Hf:E99YJFxLGoz_QyHPH7odXmCk6WBuYL3tWU0iBdpcHmb1LBph_VBAa5GyHmH6LVYP}
DEPLOYMENT_MODE = k8s
DEPLOYMENT_TARGET = prole-prod-cluster
DOCKER_IMPORT_DIR =
DOCKER_PRELOAD = false
KNOE_DB_USER = root
OPTIONAL_WORKLOADS_MIN_READY_SCHEDULABLE_NODES = 2
PROLE_K3S_SERVER = https://myrddin.prole.org:6443
PROLE_K3S_TOKEN = ${PROLE_SECRET:v1:eRF7V3BH7FObAF2t:omapQMsXQQA765nO3w8Y7LY5Hn4C1Mrglwk9xyNHEpQoKPBgrbd8aGZwtPE3UgmT_i5eeW110q0aWJtR4kwuK5q6LR9Ec2iHknIJYXA-v6isvhsPK8az1A8rE9kIVcahABT9mSEFmdNPk-UNvISsxqtOZ98fCXoFbfWegg==}
PROLE_OPENTOFU_URL = http://127.0.0.1:8080
PROLE_K3S_TOKEN = ${PROLE_SECRET:v1:dcAmcZGfYyHgaY_0:wtuc48pEhK9Ha7BC3XG57MCf_4Wqgs09g3Jam2VQIc_3jNMIEIxORsDxGJsqZtyN0zmdSxs1ApZos-h6h3R5AkEZsmqdw6TTcFpixP0RwuPWCe5ObWIMyPR3ztWcwDiM69fm1QiFUNGPvARdyw6dBgyRKRqDpVSfXMpM5w==}
SERVICE_NAMESPACE = knoe-system
SUPABASE_PV_BASE = /synology/d005
SUPABASE_PV_BASE_DIR = /synology/d005
SUPABASE_PV_NODE = gandalf.prole.org
[Welcome]
; No configuration values captured yet for this section.
@ -110,7 +115,7 @@ ANSIBLE_DOMAIN = prole.org
ANSIBLE_INFRASTRUCTURE = /Users/chrisfu/dev/prole/infrastructure
ANSIBLE_INVENTORY = /Users/chrisfu/dev/prole/infrastructure/inventory
ANSIBLE_REALM = PROLE.ORG
ANSIBLE_TOPOLOGY = {"domain":"prole.org","realm":"PROLE.ORG","internal_records":{"aventage.prole.org":"10.0.0.206","fairyland.prole.org":"10.0.0.208","loghost.prole.org":"10.0.0.3","merlin.prole.org":"10.0.0.6","morana.prole.org":"10.0.0.66","morgoth.prole.org":"10.0.0.204","myrddin.prole.org":"10.0.0.3","pi.prole.org":"10.0.0.5","raspberry.prole.org":"10.0.0.4","retropie.prole.org":"10.0.0.207","synology.prole.org":"10.0.0.203","zinfandel.prole.org":"10.0.0.205"},"ad_dc":{"host":"myrddin.prole.org","ip":"10.0.0.3"},"k3s":{"server_url":"https://myrddin.prole.org:6443","server_host":"myrddin.prole.org","token_present":true},"groups":{"iscsi":["pi.prole.org","raspberry.prole.org","myrddin.prole.org","retropie.prole.org","merlin.prole.org"],"pihole":["pi.prole.org","raspberry.prole.org"],"ad_dc":["myrddin.prole.org"],"k3s_servers":["myrddin.prole.org"],"k3s_agents":["pi.prole.org","merlin.prole.org"],"k3s_hosts:children":["k3s_servers","k3s_agents"],"linux_hosts":["pi.prole.org","raspberry.prole.org","myrddin.prole.org","retropie.prole.org","merlin.prole.org"],"ssl_hosts":["myrddin.prole.org"],"mariadb_primary":["merlin.prole.org"],"mariadb_replica":["raspberry.prole.org"],"mariadb:children":["mariadb_primary","mariadb_replica"],"merlin_bootstrap":["merlin"]},"hosts":{"merlin":"10.0.0.6","merlin.prole.org":"10.0.0.6","myrddin.prole.org":"10.0.0.3","pi.prole.org":"10.0.0.5","raspberry.prole.org":"10.0.0.4","retropie.prole.org":"10.0.0.207"},"unmapped_hosts":["k3s_agents","k3s_servers","mariadb_primary","mariadb_replica"]}
ANSIBLE_TOPOLOGY = {"domain":"prole.org","realm":"PROLE.ORG","internal_records":{"aventage.prole.org":"10.0.0.206","fairyland.prole.org":"10.0.0.208","loghost.prole.org":"10.0.0.3","merlin.prole.org":"10.0.0.6","morana.prole.org":"10.0.0.66","morgoth.prole.org":"10.0.0.204","myrddin.prole.org":"10.0.0.3","pi.prole.org":"10.0.0.5","raspberry.prole.org":"10.0.0.4","retropie.prole.org":"10.0.0.207","synology.prole.org":"10.0.0.203","zinfandel.prole.org":"10.0.0.205"},"ad_dc":{"host":"myrddin.prole.org","ip":"10.0.0.3"},"k3s":{"server_url":"https://myrddin.prole.org:6443","server_host":"myrddin.prole.org","token_present":true},"groups":{"iscsi":["pi.prole.org","raspberry.prole.org","myrddin.prole.org","retropie.prole.org","merlin.prole.org","gandalf.prole.org"],"pihole":["pi.prole.org","raspberry.prole.org"],"ad_dc":["myrddin.prole.org"],"k3s_servers":["myrddin.prole.org"],"k3s_agents":["pi.prole.org","merlin.prole.org","gandalf.prole.org"],"k3s_hosts:children":["k3s_servers","k3s_agents"],"linux_hosts":["pi.prole.org","raspberry.prole.org","myrddin.prole.org","retropie.prole.org","merlin.prole.org","gandalf.prole.org"],"ssl_hosts":["myrddin.prole.org"],"mariadb_primary":["merlin.prole.org"],"mariadb_replica":["raspberry.prole.org"],"mariadb:children":["mariadb_primary","mariadb_replica"],"merlin_bootstrap":["merlin"],"k3s_hosts":["gandalf.prole.org"]},"hosts":{"merlin":"10.0.0.6","merlin.prole.org":"10.0.0.6","myrddin.prole.org":"10.0.0.3","pi.prole.org":"10.0.0.5","raspberry.prole.org":"10.0.0.4","retropie.prole.org":"10.0.0.207"},"unmapped_hosts":["gandalf.prole.org","k3s_agents","k3s_servers","mariadb_primary","mariadb_replica"]}
KDC_ANSIBLE_DETECTED = 10.0.0.3
KDC_AUTO_DETECTED = 10.0.0.3
KERBEROS_AUTO_ENABLED = True
@ -133,7 +138,6 @@ PROLE_SERVICE = /Users/chrisfu/dev/prole/etc
[Ollama]
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
@ -152,23 +156,20 @@ GITOPS_ENABLED = False
[Dev Cluster (k3d)]
CLUSTER_ENV = dev
DISPLAY_NAME = knoe-dev-cluster
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:U7foZs_vBN4-2fLv:wFtVuuFMcJ-R9zXZwdOqxfPsTmRFZSkyvKc_Kqo_kL1-ie8MxWU2M25t78O7EjW9dFxEAYRsPanjxYKDHDd8arRW17kGIqtzlcRM9sXQJimTpa-mMgNCFew28b99c-LuCZbeG3nZU72joGZN6TZ5ZjZZbvJLAOA1Hq6b0A==}
K3S_TOKEN = ${PROLE_SECRET:v1:ZBrRQReMetk0k4X5:yl6S40jqATR45Ivg5DxoGlo_32X7Q6Zz0BpKIzWO0iKpLe5xK4H-lYTxzucn6AsivxNrZTRjLsTiQ1CrtIcmtDJzOwPjobkt5t2XGKmcBrPoeoUjqSFJolp2-0ClawERDXTnqLHOQJhNLFpqxrRc_AiU0iHiT6DuuZZ9Kg==}
MODE = k3s
PIPELINE_URL = http://127.0.0.1:8080
[Prod Cluster (k8s)]
ARTIFACTS_DIR = /Users/chrisfu/dev/prole/data/staging
CLUSTER_ENV = prole-prod-cluster
DISPLAY_NAME = prole-prod-cluster
MODE = k8s
PIPELINE_URL = http://127.0.0.1:8080
[Docker Build]
; No configuration values captured yet for this section.

View File

@ -1,5 +1,5 @@
; Prole Master Configuration File
; Generated by install.py on 2026-03-29 22:54:52
; Generated by install.py on 2026-03-31 00:16:16
; This file is used as input for Ansible deployment and k8s cluster creation.
[User]
@ -53,7 +53,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:ncw40j3021elkcqU:OO4QXoXGTi4ptxeNvqFV9WXNsh_f2QEqSYnJxIR-MW-0J50oItM9NLQeQT3rY2Hju2C2VlSyeuKi3-MBGsm3wzQl5m1yjuVQ9MKrT20u2_pjyom3GuIhfCEx6agfadRHH_9lAio3wTVdvZqBgrTRyMQ67Um07eZRsXBTUA==}
init_cluster.k3s_token = ${PROLE_SECRET:v1:AL731QFqjjFGqfFT:Xt95lYCIk-6iCHerFSJQpKwWGJMdr04v-4DawlGbM3OBLJ7qbHOSZ_oJm1WtHkvqpoM7BSJNwlEqHMV7R3gl7IFqVUtqJ_AoaRcLSPKE2Z6JGWkIatR6Vzw3J1cm1iZiM2M9wikI5Wqp3t4cdWTOTf7PUaQHkYbBG1PV3w==}
init_cluster.kerberos_enabled = true
init_cluster.mode = k3s
init_cluster.start_cluster = true
@ -64,14 +64,14 @@ init_db_build.run_build = true
init_password.cluster_name = ${CLUSTER_NAME}
init_password.db_host_port = 5432
init_password.db_namespace = ${DATABASE_NAMESPACE}
init_password.db_password = ${PROLE_SECRET:v1:4QVLv6L_Zb6w1Zja:4PHGYWVndekQb3ZG7hjNAoIQ_T2yEZlS}
init_password.db_password_confirm = ${PROLE_SECRET:v1:4QVLv6L_Zb6w1Zja:4PHGYWVndekQb3ZG7hjNAoIQ_T2yEZlS}
init_password.db_password = ${PROLE_SECRET:v1:OcWBTOiijqf-bfTT:6r-ioWdDRdkPNxmbEAQP2TYEPaycWasP}
init_password.db_password_confirm = ${PROLE_SECRET:v1:OcWBTOiijqf-bfTT:6r-ioWdDRdkPNxmbEAQP2TYEPaycWasP}
init_password.db_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 = ${PROLE_SECRET:v1:lDmjQ4Cj45LYBD9_:9Aes8WNTZ97oYpi1aFvS8ODHDCQJJJCUBFgNrJBZ3a-Mgwp2}
kerberos_config.password = ${PROLE_SECRET:v1:h9mzEWvGpnpU2FRz:5W8czWfjnRxzA8tJPk3eFRtrVLD5DxREkfqmQsnKLsbuAaec}
kerberos_config.realm = PROLE.ORG
kerberos_config.test_connection = false
kerberos_config.user = administrator
@ -79,6 +79,8 @@ network_scan.run = true
ollama_config.model =
ollama_config.server_host = fairyland.prole.org
ollama_config.server_port = 11434
supabase_config.pv_base_dir = /synology/d005
supabase_config.pv_node = gandalf.prole.org
[Global]
; Variables used by name in more than one place or assumed global scope
@ -92,7 +94,7 @@ CNPG_PLACEMENT_PLAN_ID = cnpg-placement-7036c5f65b6ef0db
CNPG_STAGE1_NODE = gandalf.prole.org
DATABASE_NAMESPACE = knoe-db
DB_HOST_PORT = 5432
DB_PASSWORD = ${PROLE_SECRET:v1:FgbkzVICzbA4WX7l:t5eAnQUYrTfAiXuUUPgZ8rBJVqAp-WFA}
DB_PASSWORD = ${PROLE_SECRET:v1:M7Rt2Xp273X8ixb5:Rjys8A6SqmBv19DvPAvgihwJ0eJ8Xvda}
DEPLOYMENT_MODE = k3s
DEPLOYMENT_TARGET = prole-service-cluster
DOCKER_PRELOAD = false
@ -100,8 +102,11 @@ KNOE_DB_USER = root
KUBECONTEXT = knoe.dev.prole.org
OPTIONAL_WORKLOADS_MIN_READY_SCHEDULABLE_NODES = 2
PROLE_K3S_SERVER = https://myrddin.prole.org:6443
PROLE_K3S_TOKEN = ${PROLE_SECRET:v1:ncw40j3021elkcqU:OO4QXoXGTi4ptxeNvqFV9WXNsh_f2QEqSYnJxIR-MW-0J50oItM9NLQeQT3rY2Hju2C2VlSyeuKi3-MBGsm3wzQl5m1yjuVQ9MKrT20u2_pjyom3GuIhfCEx6agfadRHH_9lAio3wTVdvZqBgrTRyMQ67Um07eZRsXBTUA==}
PROLE_K3S_TOKEN = ${PROLE_SECRET:v1:DkOrjdIC-ji9iusP:7uc4RUXpALlyQcGfZfX6LBP7BEwOqBtbbZpbIgdbfJvMQ_1klRdLtDQrcG54p6ayHvaMQ6W7FSPHPLeKbcjDZ8eJMhNMaH9Q0kIyfmiZdbX3nN6QGCxJWjIn-4GJ4z6_gF7hjHrqAAqxReNoWkAokyoVgqKTiOoWToD0GA==}
SERVICE_NAMESPACE = knoe-system
SUPABASE_PV_BASE = /synology/d005
SUPABASE_PV_BASE_DIR = /synology/d005
SUPABASE_PV_NODE = gandalf.prole.org
SYNOLOGY_ROOTS = /synology/d001,/synology/d002,/synology/d003,/synology/d004,/synology/d005
[Welcome]
@ -133,7 +138,7 @@ PROLE_LOGS = /Users/chrisfu/dev/prole/logs
PROLE_SERVICE = /Users/chrisfu/dev/prole/etc
[Monitoring]
; No configuration values captured yet for this section.
GRAFANA_ADMIN_PASSWORD = ${PROLE_SECRET:v1:st2uaIMwx55ftSPt:HjzyDuKpzGJyIDVzIKWD6YDzyoqZ4iJ9}
[Kerberos Authentication]
; No configuration values captured yet for this section.
@ -158,7 +163,7 @@ SUPABASE_ENABLED = False
[Initialize Cluster]
ENVIRONMENT = service
K3S_SERVER_URL = https://myrddin.prole.org:6443
K3S_TOKEN = ${PROLE_SECRET:v1:ncw40j3021elkcqU:OO4QXoXGTi4ptxeNvqFV9WXNsh_f2QEqSYnJxIR-MW-0J50oItM9NLQeQT3rY2Hju2C2VlSyeuKi3-MBGsm3wzQl5m1yjuVQ9MKrT20u2_pjyom3GuIhfCEx6agfadRHH_9lAio3wTVdvZqBgrTRyMQ67Um07eZRsXBTUA==}
K3S_TOKEN = ${PROLE_SECRET:v1:sQeWAX7UamXXV4oR:O7sUJk54DE9gUtZAjmN4uiusuVl0T3SDTn-S4Ojgg844EOQV7dMhE1wncgn8kLXXz_klqkQszDzbTqQm2HdaQuFyCqnHGCtHyUAyFvXSAdhhQ3PSkggylLtbPQPPrEP6WFuFywdKf1QgLl2BOcipREupBj5XDfiNfEAigw==}
[Dev Cluster (k3d)]
CLUSTER_ENV = dev
@ -170,7 +175,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:ncw40j3021elkcqU:OO4QXoXGTi4ptxeNvqFV9WXNsh_f2QEqSYnJxIR-MW-0J50oItM9NLQeQT3rY2Hju2C2VlSyeuKi3-MBGsm3wzQl5m1yjuVQ9MKrT20u2_pjyom3GuIhfCEx6agfadRHH_9lAio3wTVdvZqBgrTRyMQ67Um07eZRsXBTUA==}
K3S_TOKEN = ${PROLE_SECRET:v1:sast5Yrr_9mB5BzC:J8Et0XHuUbZbFzAovq-lgqdu_3rR4k3FHOJ7LmzCls-qcZV9D9_NNdmtw5gOTRA8kyZeP5oAFGAXKPGWHgxMJxGO_Lh2oQ1m_68_ExKCK7oeXxlZC3bFCd_fvQ-lP0thBYA9R-GkV_67tJfluOo632xhv6i3Pl1sgBZt5w==}
MODE = k3s
[Prod Cluster (k8s)]
@ -183,7 +188,7 @@ MODE = k8s
; No configuration values captured yet for this section.
[Initialization Scripts]
STATUS = Attempted
STATUS = Completed
[Deployment]
MODE = k3s

View File

@ -1,8 +1,6 @@
#!/usr/bin/env python3
"""
Thin launch vehicle for the Prole installer.
Keeps legacy UI available while exposing a stable import surface for tests.
"""
from __future__ import annotations
@ -13,16 +11,16 @@ from tkinter import ttk, scrolledtext, messagebox, filedialog # noqa: F401
from pathlib import Path # noqa: F401
from knoe.ui.screens import (
ProleInstaller,
ProleController,
KnoeInstaller,
KnoeController,
get_resource_path,
has_display,
main,
)
__all__ = [
"ProleInstaller",
"ProleController",
"KnoeInstaller",
"KnoeController",
"get_resource_path",
"has_display",
"main",

View File

@ -2,10 +2,74 @@
<module version="4">
<component name="AdditionalModuleElements">
<content url="file://$MODULE_DIR$" dumb="true">
<sourceFolder url="file://$MODULE_DIR$/lib/python3.14/site-packages" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/authority" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/authority/src" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/authority/src/main" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/authority/src/test" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/conf" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/docs" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/etc" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/gitea" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/infrastructure" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/k3s" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/k8s" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/knoe" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/knoe-db" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/modes" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/net" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/prod" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/prole" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/prole-app" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/prole-auth" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/prole-auth/src" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/prole-mssql-db" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/prole-net" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/prole-tools-app" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/scan" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/scripts" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/secrets" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/service" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/sql" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/supabase" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/test" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/tests" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/tools" isTestSource="false" />
<excludeFolder url="file://$MODULE_DIR$/.venv" />
<excludeFolder url="file://$MODULE_DIR$/deploy/gcp/terraform/.terraform" />
<excludeFolder url="file://$MODULE_DIR$/deploy/opentofu/k3s/.terraform" />
<excludeFolder url="file://$MODULE_DIR$/.ansible" />
<excludeFolder url="file://$MODULE_DIR$/.idea" />
<excludeFolder url="file://$MODULE_DIR$/.vagrant" />
<excludeFolder url="file://$MODULE_DIR$/.vscode" />
<excludeFolder url="file://$MODULE_DIR$/&lt;MagicMock name='mock.Entry().get().strip().strip()' id='4677834960'&gt;" />
<excludeFolder url="file://$MODULE_DIR$/bin" />
<excludeFolder url="file://$MODULE_DIR$/build" />
<excludeFolder url="file://$MODULE_DIR$/data" />
<excludeFolder url="file://$MODULE_DIR$/deploy" />
<excludeFolder url="file://$MODULE_DIR$/dev" />
<excludeFolder url="file://$MODULE_DIR$/dist" />
<excludeFolder url="file://$MODULE_DIR$/etc/secrets" />
<excludeFolder url="file://$MODULE_DIR$/htmlcov" />
<excludeFolder url="file://$MODULE_DIR$/img" />
<excludeFolder url="file://$MODULE_DIR$/include" />
<excludeFolder url="file://$MODULE_DIR$/knoe-db/data" />
<excludeFolder url="file://$MODULE_DIR$/lib" />
<excludeFolder url="file://$MODULE_DIR$/logs" />
<excludeFolder url="file://$MODULE_DIR$/mock_val" />
<excludeFolder url="file://$MODULE_DIR$/prole-tools-app" />
<excludeFolder url="file://$MODULE_DIR$/ssh-keys" />
<excludeFolder url="file://$MODULE_DIR$/tmp" />
<excludeFolder url="file://$MODULE_DIR$/vault_backup" />
</content>
</component>
<component name="FacetManager">
<facet type="Spring" name="Spring">
<configuration />
</facet>
<facet type="Python" name="Python">
<configuration sdkName="Python 3.14 (prole)" />
</facet>
</component>
</module>

View File

@ -4,17 +4,16 @@ Installer actions and unattended workflow helpers.
from __future__ import annotations
import json
import logging
import secrets
import shlex
import socket
import string
import sys
import threading
import uuid
from typing import Callable, Sequence
import logging
import secrets
import string
from knoe import prole_conf as prole_conf_mgr
from knoe.config import (
_expand_path,
_collect_cfg_vars,
@ -28,8 +27,13 @@ from knoe.config import (
_encrypt_cfg_secret,
_merge_kubeconfig,
)
from knoe import prole_conf as prole_conf_mgr
from knoe.core.controller import ProleController
from knoe.core.build_context import copy_build_context_dir
from knoe.core.cnpg_placement import (
load_cnpg_placement_plan,
plan_cnpg_placement,
save_cnpg_placement_plan,
)
from knoe.core.controller import KnoeController
from knoe.core.env import * # noqa: F401,F403
from knoe.core.env import (
_parse_ollama_host,
@ -57,7 +61,6 @@ from knoe.core.env import (
_render_prole_cfg,
_k3d_prole_data_volume_args,
_sync_opentofu_pipeline,
_normalize_k3s_token,
_read_k3s_cfg,
_safe_str,
_expand_cfg_value as _expand_env_cfg_value,
@ -80,34 +83,29 @@ from knoe.core.milestones import (
SupabaseImagePreloadMilestone,
SupabaseMilestone,
)
from knoe.core.stream_exec import run_streaming_cmd
from knoe.core.build_context import copy_build_context_dir
from knoe.core.cnpg_placement import (
load_cnpg_placement_plan,
plan_cnpg_placement,
save_cnpg_placement_plan,
)
from knoe.core.policy import (
POLICY_CFG_KEY,
OPTIONAL_WORKLOADS_MIN_READY_SCHEDULABLE_NODES,
evaluate_optional_workloads_allowed,
)
from knoe.core.ops.storage import (
ClusterStorageSpec,
StorageProvisioningError,
provision_cluster_storage,
)
from knoe.core.ops import garage_store as garage_store_ops
from knoe.core.ops import monitoring as monitoring_ops
from knoe.core.ops import openbao as openbao_ops
from knoe.core.ops import opentofu as opentofu_ops
from knoe.core.ops import registry as registry_ops
from knoe.core.ops.cloudnative_pg import (
initialize as cnpg_initialize,
deploy as cnpg_deploy,
install_barman_plugin as cnpg_install_barman_plugin,
rollout as cnpg_rollout,
)
from knoe.core.ops import openbao as openbao_ops
from knoe.core.ops import registry as registry_ops
from knoe.core.ops import garage_store as garage_store_ops
from knoe.core.ops import opentofu as opentofu_ops
from knoe.core.ops import monitoring as monitoring_ops
from knoe.core.ops.storage import (
ClusterStorageSpec,
StorageProvisioningError,
provision_cluster_storage,
)
from knoe.core.policy import (
POLICY_CFG_KEY,
OPTIONAL_WORKLOADS_MIN_READY_SCHEDULABLE_NODES,
evaluate_optional_workloads_allowed,
)
from knoe.core.stream_exec import run_streaming_cmd
from typing import Callable, Sequence
def _configure_unbuffered_io():
@ -157,7 +155,7 @@ def _select_existing_kubeconfig(candidates: Sequence[Path]) -> Path | None:
return first_existing
class ProleInstaller:
class KnoeInstaller:
"""Shared business logic for both silent and interactive installer modes.
Subclasses must provide a :meth:`_get_input` implementation that bridges
@ -3095,16 +3093,17 @@ class _TeeWriter:
return getattr(self._original, name)
class ProleConsoleInstaller(ProleInstaller):
class KnoeConsoleInstaller(KnoeInstaller):
"""Console-based unattended installer driven by prole.cfg inputs."""
silent = True
def __init__(
self,
controller: ProleController,
controller: KnoeController,
cfg_path: str | None = None,
reset_cluster: bool = False,
delete_db: bool = False,
):
self.controller = controller
self.project_root = controller.project_root
@ -3114,6 +3113,7 @@ class ProleConsoleInstaller(ProleInstaller):
self.controller.cfg_path = self.cfg_path
self.reset_cluster = bool(reset_cluster)
self.reset_requested = bool(reset_cluster)
self.delete_db_requested = bool(delete_db)
# Keep a persistent indicator for reset-oriented reconcile actions (e.g.
# reclaiming safe stale Released PV claimRefs). `_perform_cluster_reset()`
# consumes `reset_requested` and clears it, but we still want to run
@ -6025,9 +6025,15 @@ class ProleConsoleInstaller(ProleInstaller):
# Ensure we have a cert-based kubeconfig (or a valid bearer token config)
# before invoking any kubectl-based reset scripts.
self._ensure_project_k3s_kubeconfig(server, token)
self.log(
f"[RESET] Namespaces to clean (PVCs preserved): {', '.join(reset_namespaces)}"
)
if self.delete_db_requested:
self.log(f"[RESET] Namespaces to clean: {', '.join(reset_namespaces)}")
self.log(
f"[RESET] --delete-db enabled; database PVCs in namespace '{db_ns}' will be deleted."
)
else:
self.log(
f"[RESET] Namespaces to clean (PVCs preserved): {', '.join(reset_namespaces)}"
)
self.log(
f"[RESET] Preserving registry workload in namespace: {registry_ns}"
)
@ -6038,10 +6044,13 @@ class ProleConsoleInstaller(ProleInstaller):
token,
registry_namespace=registry_ns,
clear_node_reservations=True,
delete_pvcs_namespaces=[db_ns] if self.delete_db_requested else None,
force_delete_bound_pvs=self.delete_db_requested,
)
self._cleanup_local_k3s_artifacts()
self.reset_requested = False
self.delete_db_requested = False
def _cleanup_local_k3s_artifacts(self) -> None:
paths = [
@ -6571,7 +6580,7 @@ def _run_silent_install_test(project_root: Path, cfg_path: Path) -> int:
def _attempt_k3s_repair(
controller: ProleController,
controller: KnoeController,
namespace: str,
server: str,
token: str,
@ -6617,6 +6626,8 @@ def _reset_k3s_namespace(
*,
registry_namespace: str | None = None,
clear_node_reservations: bool = False,
delete_pvcs_namespaces: Sequence[str] | None = None,
force_delete_bound_pvs: bool = False,
) -> None:
script = project_root / "scripts" / "reset-ns.sh"
if not script.exists():
@ -6649,10 +6660,82 @@ def _reset_k3s_namespace(
env["KUBECONFIG"] = str(kubeconfig_path)
reg_ns = (registry_namespace or "").strip()
delete_pvcs_set = {
str(ns or "").strip()
for ns in (delete_pvcs_namespaces or [])
if str(ns or "").strip()
}
def _collect_bound_pvs(target_ns: str) -> list[str]:
res = subprocess.run(
[
"kubectl",
"-n",
target_ns,
"get",
"pvc",
"-o",
"jsonpath={range .items[*]}{.spec.volumeName}{\"\\n\"}{end}",
],
env=env,
capture_output=True,
text=True,
)
if res.returncode != 0:
print(
f"[WARN] Could not list PVC-bound PVs in {target_ns} (code {res.returncode}).",
file=sys.stderr,
)
return []
pvs: list[str] = []
for line in (res.stdout or "").splitlines():
pv = (line or "").strip()
if pv and pv not in pvs:
pvs.append(pv)
return pvs
def _set_reclaim_policy_delete(pvs: Sequence[str]) -> None:
for pv in pvs:
patch = '{"spec":{"persistentVolumeReclaimPolicy":"Delete"}}'
res = subprocess.run(
["kubectl", "patch", "pv", pv, "--type=merge", "-p", patch],
env=env,
capture_output=True,
text=True,
)
if res.returncode != 0:
print(
f"[WARN] Failed to set reclaimPolicy=Delete for pv/{pv} (code {res.returncode}).",
file=sys.stderr,
)
def _delete_pvs(pvs: Sequence[str]) -> None:
for pv in pvs:
res = subprocess.run(
["kubectl", "delete", "pv", pv, "--ignore-not-found", "--wait=false"],
env=env,
capture_output=True,
text=True,
)
if res.returncode != 0:
print(
f"[WARN] Failed to delete pv/{pv} (code {res.returncode}).",
file=sys.stderr,
)
for idx, ns in enumerate(namespaces):
delete_pvcs = ns in delete_pvcs_set
bound_pvs: list[str] = []
if delete_pvcs and force_delete_bound_pvs:
bound_pvs = _collect_bound_pvs(ns)
if bound_pvs:
_set_reclaim_policy_delete(bound_pvs)
cmd = ["bash", str(script), "-n", ns]
if reg_ns and ns == reg_ns:
cmd.append("--keep-registry")
if delete_pvcs:
cmd.append("--delete-pvcs")
if clear_node_reservations and idx == (len(namespaces) - 1):
cmd.append("--clear-node-reservations")
res = subprocess.run(cmd, env=env)
@ -6661,10 +6744,13 @@ def _reset_k3s_namespace(
f"[WARN] Namespace reset failed for {ns} (code {res.returncode})",
file=sys.stderr,
)
continue
if delete_pvcs and force_delete_bound_pvs and bound_pvs:
_delete_pvs(bound_pvs)
def _prepare_k3s_pipeline(
controller: ProleController, log_fn=None, skip_validation: bool = False
controller: KnoeController, log_fn=None, skip_validation: bool = False
) -> int:
"""Prepare OpenTofu k3s pipeline: stage manifests, tfvars, ArgoCD apps.
@ -6702,7 +6788,7 @@ def _prepare_k3s_pipeline(
# Load config to resolve namespace and secrets for pipeline tfvars
_log("==> Loading knoe config...\n")
knoe = ProleConsoleInstaller(controller, str(cfg_path))
knoe = KnoeConsoleInstaller(controller, str(cfg_path))
try:
existing_inputs = knoe._load_inputs_from_cfg()
except Exception:

View File

@ -14,14 +14,14 @@ if TYPE_CHECKING:
from knoe.milestone import Milestone
class ProleController:
class KnoeController:
"""Business logic for Prole installer, separated from UI."""
def __init__(self, project_root: Path, verbose: bool = False, cfg_path: Path | None = None):
self.project_root = project_root
self.verbose = verbose
self.cfg_path = cfg_path
self.logger = logging.getLogger("ProleController")
self.logger = logging.getLogger("KnoeController")
self.state = InstallerState(controller=self)
def check_docker_running(self) -> bool:

View File

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

View File

@ -21,23 +21,23 @@ Public API
rollout(namespace, cluster_name, env, log) -> None
"""
from __future__ import annotations
import base64
import datetime
import json
import logging
import subprocess
import time
from pathlib import Path
from typing import Callable
import json
import logging
from cryptography import x509
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric.ec import (
ECDSA,
SECP256R1,
generate_private_key,
)
from cryptography.x509.oid import NameOID
from typing import Callable
_LOG = logging.getLogger(__name__)
_LogFn = Callable[[str], None]

View File

@ -6,8 +6,8 @@ import sys
from pathlib import Path
from knoe import prole_conf
from knoe.core.controller import ProleController
from knoe.core.actions import ProleConsoleInstaller
from knoe.core.controller import KnoeController
from knoe.core.actions import KnoeConsoleInstaller
def _project_root() -> Path:
@ -80,8 +80,8 @@ def main(argv: list[str] | None = None) -> int:
if not cfg:
cfg = str(prole_conf.entrypoint_path(prole_conf.resolve_prole_conf_dir(root)))
controller = ProleController(project_root=root, verbose=bool(args.verbose), cfg_path=Path(cfg))
installer = ProleConsoleInstaller(controller, cfg_path=cfg)
controller = KnoeController(project_root=root, verbose=bool(args.verbose), cfg_path=Path(cfg))
installer = KnoeConsoleInstaller(controller, cfg_path=cfg)
try:
existing_inputs = knoe._load_inputs_from_cfg()

View File

@ -18,22 +18,21 @@ import time
from pathlib import Path
from . import config as inst_config
from .ncurses_ui import CursesWindow, NavFooter, InputField
from .core.stream_exec import run_streaming_cmd
from .config import _write_k3s_kubeconfig, _merge_kubeconfig
from .core.env import (
_resolve_k3s_connection,
_verify_k3s_services_status,
_find_kubeconfig_file,
_read_k3s_cfg,
resolve_prole_home,
)
from .core.ops import openbao as openbao_ops
from .core.ops import registry as registry_ops
from .core.ops import opentofu as opentofu_ops
from .config import _write_k3s_kubeconfig, _merge_kubeconfig
from .core.ops import registry as registry_ops
from .core.stream_exec import run_streaming_cmd
from .ncurses_ui import CursesWindow, NavFooter, InputField
class ProleNcursesInstaller:
class KnoeNcursesInstaller:
"""Ncurses-based installer matching install.py screen flow."""
def __init__(self, stdscr, controller):
@ -974,7 +973,7 @@ def run_ncurses_installer(controller):
"""Entry point for ncurses installer."""
def _main(stdscr):
installer = ProleNcursesInstaller(stdscr, controller)
installer = KnoeNcursesInstaller(stdscr, controller)
knoe.run()
try:

View File

@ -1,7 +1,7 @@
"""
Prole Service Dependencies Installer screen package.
This package splits the monolithic ``ProleInstaller`` class into cohesive
This package splits the monolithic ``KnoeInstaller`` class into cohesive
mixin modules, one per logical screen / concern. The composed class is
assembled here and re-exported so that external callers (``install.py``,
tests) see the same public surface as before.
@ -10,7 +10,6 @@ tests) see the same public surface as before.
from __future__ import annotations
import configparser
import platform
import signal
import socket
import threading
@ -18,45 +17,18 @@ import tkinter as tk
import uuid
from tkinter import ttk, messagebox, filedialog
from knoe import deploy as inst_deploy
from knoe import prole_conf
import platform
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,
_decrypt_prole_secret,
_is_openbao_ref,
_write_k3s_kubeconfig,
_encrypt_cfg_secret,
_encrypt_prole_secret,
)
from knoe.core.controller import ProleController
from knoe.core.controller import KnoeController
from knoe.core.env import (
_detect_ansible_topology,
_format_ansible_topology_summary,
_normalize_k3s_token,
_deployment_target_label,
_cluster_env_radio_value,
_find_kubeconfig_file,
_bool_str,
_looks_like_k8s_bearer_token,
_deployment_mode_from_env,
_pf_extract_id,
_ensure_ansible_vault_credentials,
_openbao_placeholder,
_pf_upsert_mapping,
_default_opentofu_pipeline_url,
_build_required_port_forwards,
_render_prole_cfg,
_safe_str,
_normalize_cluster_env,
_k3d_prole_data_volume_args,
_push_docker_image,
_http_ping_registry,
_local_registry_enabled,
_collect_images_from_files,
)
# On macOS, set process name early so the menu bar reads correctly.
@ -70,10 +42,9 @@ if platform.system() == "Darwin":
from knoe.core.env import * # noqa: F401,F403
from knoe.core.actions import (
ProleInstaller,
ProleConsoleInstaller,
KnoeInstaller,
KnoeConsoleInstaller,
_prepare_k3s_pipeline,
_reset_k3s_namespace,
)
# Keep global frames list in memory (legacy requirement).
@ -108,8 +79,8 @@ from knoe.ui.screens.cfg import ConfigMixin
# ---------------------------------------------------------------------------
# Composed installer class
# ---------------------------------------------------------------------------
class ProleInstaller(
ProleInstaller,
class KnoeInstaller(
KnoeInstaller,
ScreenBaseMixin,
NavigationMixin,
WelcomeScreenMixin,
@ -212,7 +183,7 @@ class ProleInstaller(
conf_dir = prole_conf.resolve_prole_conf_dir(PROJECT_ROOT)
resolved_cfg = prole_conf.entrypoint_path(conf_dir)
self.controller = ProleController(
self.controller = KnoeController(
PROJECT_ROOT, verbose=verbose, cfg_path=resolved_cfg
)
self.project_root = PROJECT_ROOT
@ -734,6 +705,21 @@ class ProleInstaller(
self.kerberos_password = tk.StringVar()
self.kerberos_kdc = tk.StringVar()
self.supabase_enabled = tk.BooleanVar(value=False)
_global_cfg = self.prole_cfg_data.get("Global", {}) or {}
_supabase_pv_node = (
(_global_cfg.get("SUPABASE_PV_NODE", "") or "").strip()
or (os.environ.get("SUPABASE_PV_NODE") or "").strip()
or "gandalf.prole.org"
)
_supabase_pv_base = (
(_global_cfg.get("SUPABASE_PV_BASE_DIR", "") or "").strip()
or (_global_cfg.get("SUPABASE_PV_BASE", "") or "").strip()
or (os.environ.get("SUPABASE_PV_BASE_DIR") or "").strip()
or (os.environ.get("SUPABASE_PV_BASE") or "").strip()
or "/synology/d005"
)
self.supabase_pv_node = tk.StringVar(value=_supabase_pv_node)
self.supabase_pv_base_dir = tk.StringVar(value=_supabase_pv_base)
self.at_rest_encryption_enabled = tk.BooleanVar(value=True)
self.ollama_server_host = tk.StringVar()
self.ollama_server_port = tk.StringVar(value=DEFAULT_OLLAMA_PORT)
@ -891,6 +877,12 @@ def main():
action="store_true",
help="Reset k3d dev cluster or k3s service cluster",
)
parser.add_argument(
"--delete-db",
action="store_true",
default=False,
help=argparse.SUPPRESS,
)
parser.add_argument(
"--no-gui",
action="store_true",
@ -919,6 +911,9 @@ def main():
parser.add_argument("command_args", nargs="*", help="Arguments for the subcommand")
args = parser.parse_args()
if args.delete_db and not args.reset:
parser.error("--delete-db requires --reset")
# Set up log file tee if --log is requested
log_file_path = None
if args.log is not None:
@ -933,7 +928,7 @@ def main():
setup_logging(verbose=args.verbose, debug=args.debug)
controller = ProleController(
controller = KnoeController(
PROJECT_ROOT,
verbose=args.verbose,
cfg_path=Path(args.config).expanduser() if args.config else None,
@ -953,8 +948,11 @@ def main():
if args.silent:
_ensure_ansible_vault_credentials(prompt_ui=False)
installer = ProleConsoleInstaller(
controller, args.config, reset_cluster=args.reset
installer = KnoeConsoleInstaller(
controller,
args.config,
reset_cluster=args.reset,
delete_db=args.delete_db,
)
if log_file_path:
installer.enable_log_file(log_file_path)
@ -994,7 +992,7 @@ def main():
except Exception:
root.geometry(f"{window_width}x{window_height}")
_ensure_ansible_vault_credentials(prompt_ui=True, root=root)
ProleInstaller(
KnoeInstaller(
root,
config_path=args.config,
reset_cluster=args.reset,

View File

@ -2,10 +2,10 @@
import os
import subprocess
from datetime import datetime
import tkinter as tk
from datetime import datetime
from tkinter import ttk, messagebox, filedialog
from knoe.screen import TerminalConsole
from knoe import screen as ui

View File

@ -1,20 +1,20 @@
"""Build screen, terminal console overlay and process management."""
import os
import platform
import re
import shlex
import signal
import subprocess
import threading
import time
import webbrowser
import tkinter as tk
import webbrowser
from tkinter import ttk, messagebox, filedialog
from knoe.screen import TerminalConsole
import platform
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.build import get_build_command as inst_get_build_command
from knoe.core.env import PROJECT_ROOT

View File

@ -2,14 +2,17 @@
import configparser
import os
import platform
import socket
import string
import subprocess
from pathlib import Path
import tkinter as tk
from pathlib import Path
from tkinter import ttk, messagebox, filedialog
from knoe import prole_conf
import platform
from knoe.config import (
_encrypt_cfg_secret,
_filter_cfg_values_for_persistence,
)
from knoe.core.env import (
DEFAULT_ACTION_FLAGS,
DEFAULT_OLLAMA_PORT,
@ -28,12 +31,6 @@ from knoe.core.env import (
_read_k3s_cfg,
_render_prole_cfg,
)
from knoe.config import (
_collect_cfg_vars,
_encrypt_cfg_secret,
_expand_cfg_value,
_filter_cfg_values_for_persistence,
)
class ConfigMixin:
@ -112,6 +109,9 @@ class ConfigMixin:
"DATABASE_NAMESPACE": (self.db_namespace.get() or "").strip(),
"CLUSTER_NAME": (self.cnpg_cluster_name.get() or "").strip(),
"DB_HOST_PORT": (self.db_host_port.get() or "5432").strip(),
"SUPABASE_PV_NODE": (self.supabase_pv_node.get() or "").strip(),
"SUPABASE_PV_BASE": (self.supabase_pv_base_dir.get() or "").strip(),
"SUPABASE_PV_BASE_DIR": (self.supabase_pv_base_dir.get() or "").strip(),
"PROLE_OPENTOFU_URL": _default_opentofu_pipeline_url(),
}
if prole_home_override:
@ -477,6 +477,8 @@ class ConfigMixin:
DEFAULT_ACTION_FLAGS.get("init_cluster.start_cluster", True),
),
)
_set("supabase_config.pv_node", _get_var(self.supabase_pv_node, ""))
_set("supabase_config.pv_base_dir", _get_var(self.supabase_pv_base_dir, ""))
# Kerberos config
_set_bool("kerberos_config.enabled", _get_var(self.kerberos_enabled, False))

View File

@ -1,34 +1,18 @@
"""Cluster lifecycle management (k3d / k3s / k8s)."""
import json
import os
import re
import subprocess
import sys
import threading
import time
import urllib.error
import urllib.request
from pathlib import Path
import tkinter as tk
from pathlib import Path
from tkinter import ttk, messagebox, filedialog
from knoe.screen import TerminalConsole
from knoe import screen as ui
import json
from knoe import prole_conf
from knoe.core.env import (
PROJECT_ROOT,
_default_opentofu_pipeline_url,
_deployment_mode_from_env,
_deployment_target_label,
_find_kubeconfig_file,
_k3d_prole_data_volume_args,
_kubectl_base_cmd_for_k3s as _kubectl_base_cmd_for_k3s_fn,
_looks_like_k8s_bearer_token,
_normalize_cluster_env,
_normalize_k3s_token,
_resolve_k3s_connection as _resolve_k3s_connection_fn,
_safe_str,
)
from knoe import screen as ui
from knoe.config import (
_collect_cfg_vars_from_data,
_encrypt_cfg_secret,
@ -37,6 +21,18 @@ from knoe.config import (
_write_k3s_kubeconfig,
)
from knoe.core.actions import _reset_k3s_namespace
from knoe.core.env import (
PROJECT_ROOT,
_default_opentofu_pipeline_url,
_deployment_mode_from_env,
_deployment_target_label,
_find_kubeconfig_file,
_k3d_prole_data_volume_args,
_normalize_cluster_env,
_normalize_k3s_token,
_resolve_k3s_connection as _resolve_k3s_connection_fn,
_safe_str,
)
from knoe.core.prod_config import ProdConfigApi, build_config, to_api_payload
@ -801,7 +797,10 @@ class ClusterScreenMixin:
self._k3s_service_status_console = status_console
self._k3s_service_deploy_console = deploy_console
self._common_services_success = bool(getattr(self, "_all_services_green", False))
self._common_services_success = bool(
getattr(self, "_common_services_success", False)
or getattr(self, "_all_services_green", False)
)
self.update_footer()
self._verify_k3s_services()
@ -845,7 +844,7 @@ class ClusterScreenMixin:
return key
def _is_valid_k8s_namespace(self, value: str | None) -> bool:
ns = (value or "").strip()
ns = _safe_str(value).strip()
if not ns or len(ns) > 63:
return False
return re.fullmatch(r"[a-z0-9]([-a-z0-9]*[a-z0-9])?", ns) is not None

View File

@ -1,13 +1,13 @@
"""Database Options screen for selecting distribution, version and extensions."""
import json
import os
import re
import threading
from pathlib import Path
import tkinter as tk
from pathlib import Path
from tkinter import ttk, messagebox
import json
from knoe import screen as ui
from knoe.core.build_context import copy_build_context_dir
from knoe.core.env import (
@ -18,6 +18,7 @@ from knoe.core.env import (
resolve_prole_home,
)
class DatabaseOptionsScreenMixin:
"""Mixin for the Database Options screen."""

View File

@ -2,12 +2,12 @@
import os
import plistlib
import secrets
import subprocess
import threading
from pathlib import Path
import tkinter as tk
from pathlib import Path
from tkinter import ttk, messagebox, filedialog
from knoe import screen as ui
from knoe.core.env import PROJECT_ROOT

View File

@ -1,7 +1,6 @@
"""Docker image build, registry management and image pre-pull."""
import os
import platform
import re
import shutil
import subprocess
@ -12,7 +11,6 @@ from pathlib import Path
from knoe.config import get_docker_build_platform_args
from knoe.core.env import (
PROJECT_ROOT,
_collect_images_from_files,
_deployment_mode_from_env,
_find_kubeconfig_file,
_host_from_url,
@ -21,7 +19,6 @@ from knoe.core.env import (
_local_registry_enabled,
_normalize_cluster_env,
_push_docker_image,
_resolve_supabase_home,
)

View File

@ -1,20 +1,20 @@
"""Disk selection, installer packaging, DMG creation and build summary."""
import os
import platform
import plistlib
import shutil
import subprocess
import sys
import threading
import time
import tkinter as tk
import webbrowser
from pathlib import Path
import tkinter as tk
from tkinter import ttk, messagebox, filedialog
from knoe.screen import TerminalConsole
from knoe import screen as ui
import platform
from knoe import config as inst_config
from knoe import screen as ui
from knoe.core.env import PROJECT_ROOT, get_resource_path

View File

@ -1,18 +1,17 @@
"""Initialization scripts, CNPG deployment and service-layer overlays."""
import os
import platform
import secrets
import shlex
import subprocess
import threading
import time
import webbrowser
from pathlib import Path
from datetime import datetime
import tkinter as tk
import webbrowser
from datetime import datetime
from pathlib import Path
from tkinter import ttk, messagebox, filedialog
from knoe.screen import TerminalConsole
import platform
from knoe import screen as ui
from knoe.core.env import (
PROJECT_ROOT,
@ -21,21 +20,21 @@ from knoe.core.env import (
_deployment_target_label,
_normalize_cluster_env,
)
from knoe.core.policy import (
POLICY_CFG_KEY,
OPTIONAL_WORKLOADS_MIN_READY_SCHEDULABLE_NODES,
evaluate_optional_workloads_allowed,
)
from knoe.core.ops import garage_store as garage_store_ops
from knoe.core.ops import monitoring as monitoring_ops
from knoe.core.ops import openbao as openbao_ops
from knoe.core.ops import opentofu as opentofu_ops
from knoe.core.ops import registry as registry_ops
from knoe.core.ops.cloudnative_pg import (
initialize as cnpg_initialize,
deploy as cnpg_deploy,
rollout as cnpg_rollout,
)
from knoe.core.ops import openbao as openbao_ops
from knoe.core.ops import registry as registry_ops
from knoe.core.ops import garage_store as garage_store_ops
from knoe.core.ops import opentofu as opentofu_ops
from knoe.core.ops import monitoring as monitoring_ops
from knoe.core.policy import (
POLICY_CFG_KEY,
OPTIONAL_WORKLOADS_MIN_READY_SCHEDULABLE_NODES,
evaluate_optional_workloads_allowed,
)
class ServicesScreenMixin:

View File

@ -63,6 +63,54 @@ class SupabaseScreenMixin:
self._overlay_widgets.append(enable_cb)
y = 280
node_label = ui.canvas_text(
self,
48,
y,
"Node selector:",
fill="black",
font=("SF Pro Text", 12),
)
self._canvas_items.append(node_label)
node_entry = tk.Entry(
self.bg_canvas,
textvariable=self.supabase_pv_node,
font=("SF Pro Text", 11),
width=38,
relief="solid",
bd=1,
)
node_entry_window = self.bg_canvas.create_window(
190, y - 2, window=node_entry, anchor="nw"
)
self._canvas_items.append(node_entry_window)
self._overlay_widgets.append(node_entry)
y += 36
storage_label = ui.canvas_text(
self,
48,
y,
"Storage directory:",
fill="black",
font=("SF Pro Text", 12),
)
self._canvas_items.append(storage_label)
storage_entry = tk.Entry(
self.bg_canvas,
textvariable=self.supabase_pv_base_dir,
font=("SF Pro Text", 11),
width=38,
relief="solid",
bd=1,
)
storage_entry_window = self.bg_canvas.create_window(
190, y - 2, window=storage_entry, anchor="nw"
)
self._canvas_items.append(storage_entry_window)
self._overlay_widgets.append(storage_entry)
y += 42
initial_status = "Ready" if self.supabase_enabled.get() else "Disabled"
self._supabase_status_var = tk.StringVar(value=initial_status)
status_item = ui.canvas_text(
@ -86,10 +134,10 @@ class SupabaseScreenMixin:
# Standardized Console Output
self._supabase_console = self._create_console_output(
y=320, title="Deployment Output", width=880, height=380
y=390, title="Deployment Output", width=880, height=310
)
y = 740
y = 720
# Deploy Supabase Button
self._supabase_deploy_button = tk.Button(
@ -143,7 +191,27 @@ class SupabaseScreenMixin:
env["PROLE_SERVICE"] = str(PROJECT_ROOT)
env["NAMESPACE"] = namespace
pv_node = ""
try:
pv_node = (self.supabase_pv_node.get() or "").strip()
except Exception:
pv_node = ""
if pv_node:
env["SUPABASE_PV_NODE"] = pv_node
pv_base_dir = ""
try:
pv_base_dir = (self.supabase_pv_base_dir.get() or "").strip()
except Exception:
pv_base_dir = ""
if pv_base_dir:
env["SUPABASE_PV_BASE_DIR"] = pv_base_dir
env["SUPABASE_PV_BASE"] = pv_base_dir
self._supabase_console.write("Starting Supabase deployment...\n")
self._supabase_console.write(
f"Placement: node={pv_node or 'auto'} storage={pv_base_dir or 'auto'}\n"
)
script_path = PROJECT_ROOT / "supabase" / "deploy.sh"
if not script_path.exists():
@ -191,6 +259,12 @@ class SupabaseScreenMixin:
cfg_path = None
if cfg_path:
args.extend(["-c", str(cfg_path)])
try:
self._save_prole_cfg()
except Exception:
pass
if _parse_bool(os.environ.get("SUPABASE_USE_DEV_COMPOSE"), False):
args.append("--with-dev-helpers")
if _parse_bool(os.environ.get("SUPABASE_FOREGROUND"), False):

View File

@ -3,10 +3,10 @@
import subprocess
import threading
import time
import webbrowser
import tkinter as tk
import webbrowser
from tkinter import ttk, messagebox, filedialog
from knoe.screen import TerminalConsole
from knoe import screen as ui

View File

@ -1 +1 @@
150
158

View File

@ -1,3 +1,3 @@
from prole.deployment import ProleDeployment
from prole.deployment import KnoeDeployment
__all__ = ["ProleDeployment"]
__all__ = ["KnoeDeployment"]

View File

@ -1,14 +1,13 @@
from __future__ import annotations
import argparse
import logging
import os
import sys
from pathlib import Path
from knoe.core.controller import ProleController
from knoe.core.actions import KnoeConsoleInstaller, _prepare_k3s_pipeline
from knoe.core.controller import KnoeController
from knoe.core.env import _ensure_ansible_vault_credentials, PROJECT_ROOT
from knoe.core.actions import ProleConsoleInstaller, _prepare_k3s_pipeline
def main():
@ -44,6 +43,12 @@ def main():
action="store_true",
help="Reset k3d dev cluster or k3s service cluster",
)
parser.add_argument(
"--delete-db",
action="store_true",
default=False,
help=argparse.SUPPRESS,
)
parser.add_argument(
"--no-gui",
action="store_true",
@ -63,6 +68,9 @@ def main():
prole_conf = os.environ.get("PROLE_CONF")
args = parser.parse_args()
if args.delete_db and not args.reset:
parser.error("--delete-db requires --reset")
if args.config is None and prole_conf:
potential_cfg = Path(prole_conf) / "prole.cfg"
if potential_cfg.exists():
@ -72,16 +80,16 @@ def main():
setup_logging(verbose=args.verbose, debug=args.debug)
controller = ProleController(
controller = KnoeController(
PROJECT_ROOT,
verbose=args.verbose,
cfg_path=Path(args.config).expanduser() if args.config else None,
)
if args.command == "deploy":
from prole.deployment import ProleDeployment
from prole.deployment import KnoeDeployment
deployment = ProleDeployment(controller, PROJECT_ROOT)
deployment = KnoeDeployment(controller, PROJECT_ROOT)
if deployment.duplicate_k3d_to_k3s():
if deployment.apply():
sys.exit(0)
@ -89,8 +97,11 @@ def main():
if args.silent:
_ensure_ansible_vault_credentials(prompt_ui=False)
installer = ProleConsoleInstaller(
controller, args.config, reset_cluster=args.reset
installer = KnoeConsoleInstaller(
controller,
args.config,
reset_cluster=args.reset,
delete_db=args.delete_db,
)
if args.log:
log_path = (

View File

@ -1,17 +1,16 @@
from __future__ import annotations
import configparser
import logging
import os
import subprocess
from pathlib import Path
import logging
from knoe import prole_conf
from knoe.core.actions import ProleInstaller
from knoe.core.actions import KnoeInstaller
from knoe.core.env import (
_detect_ansible_topology,
_sync_opentofu_pipeline,
_normalize_cluster_env,
_parse_bool,
)
from knoe.core.ops.cloudnative_pg import initialize as cnpg_initialize
@ -40,7 +39,7 @@ _CFG_KEY_MAP = {
}
class ProleDeployment(ProleInstaller):
class KnoeDeployment(KnoeInstaller):
"""
Top-level class for Prole infrastructure deployment via OpenTofu.
Duplicate k3d stack and apply to k3s defined in Ansible inventory.

View File

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

View File

@ -90,7 +90,8 @@ spec:
image: "{{ .Values.image.storage.repository }}:{{ .Values.image.storage.tag | default .Chart.AppVersion }}"
imagePullPolicy: {{ .Values.image.storage.pullPolicy }}
env:
{{- range $key, $value := .Values.environment.storage }}
{{- $storageEnv := (.Values.environment.storage | default dict) }}
{{- range $key, $value := $storageEnv }}
{{- if and (ne $key "DB_HOST") (ne $key "DB_PORT") (ne $key "DB_SSL") }}
- name: {{ $key }}
value: {{ $value | quote }}
@ -193,16 +194,24 @@ spec:
value: {{ printf "http://%s:%v" (include "supabase.imgproxy.fullname" .) (.Values.service.imgproxy.port | int) | quote }}
{{- end }}
{{- if not (hasKey $storageEnv "STORAGE_BACKEND") }}
- name: STORAGE_BACKEND
value: {{ if .Values.deployment.minio.enabled }}s3{{ else }}file{{ end }}
{{- end }}
{{- if .Values.deployment.minio.enabled }}
{{- if not (hasKey $storageEnv "GLOBAL_S3_ENDPOINT") }}
- name: GLOBAL_S3_ENDPOINT
value: {{ printf "http://%s:%v" (include "supabase.minio.fullname" .) (default 9000 .Values.service.minio.port) | quote }}
{{- end }}
{{- if not (hasKey $storageEnv "GLOBAL_S3_PROTOCOl") }}
- name: GLOBAL_S3_PROTOCOl
value: http
{{- end }}
{{- if not (hasKey $storageEnv "GLOBAL_S3_FORCE_PATH_STYLE") }}
- name: GLOBAL_S3_FORCE_PATH_STYLE
value: "true"
{{- end }}
- name: AWS_ACCESS_KEY_ID
valueFrom:
secretKeyRef:

View File

@ -67,6 +67,45 @@ def _is_placeholder(value: str) -> bool:
return v.startswith("${") and v.endswith("}")
def _as_bool(value: str | bool | None, default: bool = False) -> bool:
if isinstance(value, bool):
return value
if value is None:
return default
normalized = str(value).strip().lower()
if not normalized:
return default
if normalized in {"1", "true", "yes", "on", "y"}:
return True
if normalized in {"0", "false", "no", "off", "n"}:
return False
return default
def _parse_hosts(raw_hosts: str, fallback: str = "db.prole.org") -> list[str]:
entries: list[str] = []
seen: set[str] = set()
for entry in (raw_hosts or "").split(","):
token = (entry or "").strip()
if not token or _is_placeholder(token):
continue
if "://" in token:
parsed = urllib.parse.urlparse(token)
host = (parsed.hostname or "").strip()
else:
host = token
if not host or host in seen:
continue
seen.add(host)
entries.append(host)
if entries:
return entries
return [fallback]
def _discover_k8s_service_namespace(service_name: str) -> str:
try:
raw = subprocess.check_output(["kubectl", "get", "svc", "-A", "-o", "json"], text=True)
@ -161,7 +200,9 @@ def _build_overlay(cfg: configparser.ConfigParser, args: argparse.Namespace) ->
anon_key = _jwt(anon_payload, jwt_secret)
service_key = _jwt(service_payload, jwt_secret)
supabase_hostname = _first(
supabase_hostname_raw = _first(
os.environ.get("SUPABASE_STUDIO_URL", ""),
_cfg_get(cfg, "Inputs", "init_cluster.supabase_studio_url"),
_cfg_get(cfg, "User", "supabase_hostname"),
_cfg_get(cfg, "Global", "supabase_hostname"),
_cfg_get(cfg, "User", "SUPABASE_HOSTNAME"),
@ -171,16 +212,73 @@ def _build_overlay(cfg: configparser.ConfigParser, args: argparse.Namespace) ->
default="db.prole.org",
).strip()
if _is_placeholder(supabase_hostname):
supabase_hostname = ""
if not supabase_hostname:
supabase_hostname = "db.prole.org"
if _is_placeholder(supabase_hostname_raw):
supabase_hostname_raw = ""
if not supabase_hostname_raw:
supabase_hostname_raw = "db.prole.org"
studio_enabled = _as_bool(
_first(
os.environ.get("SUPABASE_STUDIO_ENABLED", ""),
_cfg_get(cfg, "Inputs", "init_cluster.supabase_studio_enabled"),
_cfg_get(cfg, "Global", "SUPABASE_STUDIO_ENABLED"),
default="true",
),
default=True,
)
auth_enabled = _as_bool(
_first(
os.environ.get("SUPABASE_AUTH_ENABLED", ""),
_cfg_get(cfg, "Inputs", "init_cluster.supabase_auth_enabled"),
_cfg_get(cfg, "Global", "SUPABASE_AUTH_ENABLED"),
default="true",
),
default=True,
)
realtime_enabled = _as_bool(
_first(
os.environ.get("SUPABASE_REALTIME_ENABLED", ""),
_cfg_get(cfg, "Inputs", "init_cluster.supabase_realtime_enabled"),
_cfg_get(cfg, "Global", "SUPABASE_REALTIME_ENABLED"),
default="true",
),
default=True,
)
meta_enabled = _as_bool(
_first(
os.environ.get("SUPABASE_META_ENABLED", ""),
_cfg_get(cfg, "Inputs", "init_cluster.supabase_meta_enabled"),
_cfg_get(cfg, "Global", "SUPABASE_META_ENABLED"),
default="true",
),
default=True,
)
analytics_enabled = _as_bool(
_first(
os.environ.get("SUPABASE_ANALYTICS_ENABLED", ""),
_cfg_get(cfg, "Inputs", "init_cluster.supabase_analytics_enabled"),
_cfg_get(cfg, "Global", "SUPABASE_ANALYTICS_ENABLED"),
default="true",
),
default=True,
)
raw_host_entries: list[str] = []
for entry in supabase_hostname_raw.split(","):
token = (entry or "").strip()
if token and not _is_placeholder(token):
raw_host_entries.append(token)
if not raw_host_entries:
raw_host_entries = ["db.prole.org"]
host_entries = _parse_hosts(",".join(raw_host_entries), fallback="db.prole.org")
ingress_host = host_entries[0]
# Canonical public URL for Supabase (used for auth callbacks + Studio links).
public_url = ""
ingress_host = supabase_hostname
if "://" in supabase_hostname:
parsed = urllib.parse.urlparse(supabase_hostname)
primary_raw_host = (raw_host_entries[0] or "").strip()
if "://" in primary_raw_host:
parsed = urllib.parse.urlparse(primary_raw_host)
ingress_host = (parsed.hostname or "").strip() or ingress_host
scheme = (parsed.scheme or "https").strip()
if parsed.netloc:
@ -220,9 +318,13 @@ def _build_overlay(cfg: configparser.ConfigParser, args: argparse.Namespace) ->
).strip().lower()
supabase_primary_node = _first(
os.environ.get("SUPABASE_PRIMARY_NODE", ""),
os.environ.get("SUPABASE_NODE_SELECTOR", ""),
os.environ.get("SUPABASE_PV_NODE", ""),
_cfg_get(cfg, "Global", "SUPABASE_PRIMARY_NODE"),
_cfg_get(cfg, "Global", "SUPABASE_NODE_SELECTOR"),
_cfg_get(cfg, "Global", "SUPABASE_PV_NODE"),
_cfg_get(cfg, "Supabase", "PRIMARY_NODE"),
default="merlin.prole.org",
default="gandalf.prole.org",
).strip()
if _is_placeholder(supabase_primary_node):
supabase_primary_node = ""
@ -233,7 +335,7 @@ def _build_overlay(cfg: configparser.ConfigParser, args: argparse.Namespace) ->
os.environ.get("SUPABASE_STORAGE_CLASS", ""),
_cfg_get(cfg, "Global", "SUPABASE_STORAGE_CLASS"),
_cfg_get(cfg, "Supabase", "STORAGE_CLASS"),
default="merlin-local-iscsi-d002",
default="synology-iscsi",
).strip()
if _is_placeholder(supabase_storage_class):
supabase_storage_class = "merlin-local-iscsi-d002"
@ -243,20 +345,20 @@ def _build_overlay(cfg: configparser.ConfigParser, args: argparse.Namespace) ->
"fullnameOverride": "supabase",
"deployment": {
"db": {"enabled": False},
"functions": {"enabled": False},
"vector": {"enabled": False},
"kong": {"enabled": False},
"storage": {"enabled": False},
"minio": {"enabled": False},
"imgproxy": {"enabled": False},
"functions": {"enabled": True},
"vector": {"enabled": True},
"kong": {"enabled": True},
"storage": {"enabled": True},
"minio": {"enabled": True},
"imgproxy": {"enabled": True},
# Explicit fullnameOverride per component strips the chart name
# from pod names (avoids 'supabase-prole-supabase-<component>').
"analytics": {"fullnameOverride": "supabase-analytics"},
"auth": {"fullnameOverride": "supabase-auth"},
"meta": {"fullnameOverride": "supabase-meta"},
"realtime": {"fullnameOverride": "supabase-realtime"},
"rest": {"fullnameOverride": "supabase-rest"},
"studio": {"fullnameOverride": "supabase-studio"},
"analytics": {"enabled": analytics_enabled, "fullnameOverride": "supabase-analytics"},
"auth": {"enabled": auth_enabled, "fullnameOverride": "supabase-auth"},
"meta": {"enabled": meta_enabled, "fullnameOverride": "supabase-meta"},
"realtime": {"enabled": realtime_enabled, "fullnameOverride": "supabase-realtime"},
"rest": {"enabled": True, "fullnameOverride": "supabase-rest"},
"studio": {"enabled": studio_enabled, "fullnameOverride": "supabase-studio"},
},
"externalDatabase": {
"enabled": True,
@ -287,9 +389,12 @@ def _build_overlay(cfg: configparser.ConfigParser, args: argparse.Namespace) ->
"ingress": {
"enabled": True,
"className": "traefik",
"hosts": [
{"host": ingress_host, "paths": [{"path": "/", "pathType": "Prefix"}]}
],
"hosts": [{"host": host, "paths": [{"path": "/", "pathType": "Prefix"}]} for host in host_entries],
},
"studioIngress": {
"enabled": True,
"className": "traefik",
"hosts": [{"host": host, "paths": [{"path": "/", "pathType": "Prefix"}]} for host in host_entries],
},
}
@ -304,20 +409,24 @@ def _build_overlay(cfg: configparser.ConfigParser, args: argparse.Namespace) ->
)
# Write to persistence.*.storageClassName — the path the Helm chart actually uses
persistence = overlay.setdefault("persistence", {})
# Only set storageClass for components that are still deployed; storage/minio/imgproxy are removed.
for pvc_key in ("functions", "snippets", "deno"):
for pvc_key in ("functions", "snippets", "deno", "imgproxy", "minio", "storage"):
persistence.setdefault(pvc_key, {})["storageClassName"] = supabase_storage_class
if mode == "k3s" and supabase_node_selector:
deployment = overlay.setdefault("deployment", {})
# Only pin active control-plane components; storage/minio/imgproxy/kong are disabled.
for component in (
"analytics",
"auth",
"functions",
"imgproxy",
"kong",
"meta",
"minio",
"realtime",
"rest",
"storage",
"studio",
"vector",
):
component_cfg = deployment.setdefault(component, {})
component_cfg.setdefault("nodeSelector", {})

View File

@ -23,9 +23,9 @@ def test_actions_save_env_to_file_expands_shell_vars(monkeypatch, tmp_path: Path
)
(proj / "etc" / "dummy.txt").write_text("ok\n")
from knoe.core.actions import ProleInstaller
from knoe.core.actions import KnoeInstaller
class Dummy(ProleInstaller):
class Dummy(KnoeInstaller):
def __init__(self, project_root: Path):
self.project_root = project_root
self.inputs = {"init_cluster.cluster_env": "dev"}

View File

@ -1,6 +1,7 @@
"""Tests for installer/core/actions.py ProleInstallerBase helper methods."""
from __future__ import annotations
import os
import subprocess
import sys
@ -8,11 +9,9 @@ from pathlib import Path
from unittest import mock
from unittest.mock import MagicMock, patch
import pytest
from knoe.core.actions import (
ProleInstaller,
ProleConsoleInstaller,
KnoeInstaller,
KnoeConsoleInstaller,
_configure_unbuffered_io,
_select_existing_kubeconfig,
)
@ -23,7 +22,7 @@ from knoe.core.actions import (
# ---------------------------------------------------------------------------
class _TestableInstaller(ProleInstaller):
class _TestableInstaller(KnoeInstaller):
"""Minimal concrete subclass for testing base-class helpers."""
def __init__(self, inputs=None, project_root=None):
@ -329,8 +328,8 @@ spec:
# ---------------------------------------------------------------------------
class _TestableSilentInstaller(ProleConsoleInstaller):
"""Testable subclass of ProleConsoleInstaller."""
class _TestableSilentInstaller(KnoeConsoleInstaller):
"""Testable subclass of KnoeConsoleInstaller."""
def __init__(self, inputs=None, project_root=None):
self._inputs_dict = inputs or {}

View File

@ -1,5 +1,5 @@
import pytest
from unittest.mock import patch, MagicMock
from knoe.config import (
normalize_version,
is_apple_silicon,

View File

@ -1,15 +1,13 @@
"""Extended tests for installer/config.py covering encryption, properties, helpers."""
from __future__ import annotations
import configparser
import os
import platform
from pathlib import Path
from unittest import mock
from unittest.mock import MagicMock, patch
import pytest
from knoe.config import (
_is_prole_secret,
_is_openbao_ref,

View File

@ -13,7 +13,6 @@ import os
import subprocess
from pathlib import Path
from unittest.mock import MagicMock, patch
import pytest
from knoe.config import (
_get_secret_key_file,

View File

@ -1,7 +1,7 @@
"""
Unit tests for installer/core/controller.py
Covers ProleController: check_docker_running, get_knoe_db_version, run_script.
Covers KnoeController: check_docker_running, get_knoe_db_version, run_script.
"""
from __future__ import annotations
@ -9,17 +9,16 @@ import os
import subprocess
from pathlib import Path
from unittest.mock import MagicMock, patch
import pytest
from knoe.core.controller import ProleController
from knoe.core.controller import KnoeController
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
def _make_controller(tmp_path: Path, **kwargs) -> ProleController:
return ProleController(project_root=tmp_path, **kwargs)
def _make_controller(tmp_path: Path, **kwargs) -> KnoeController:
return KnoeController(project_root=tmp_path, **kwargs)
def _scaffold_project(tmp_path: Path) -> None:

View File

@ -20,9 +20,9 @@ def test_controller_run_script_expands_shell_vars(monkeypatch, tmp_path: Path):
(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
from knoe.core.controller import KnoeController
controller = ProleController(project_root=proj)
controller = KnoeController(project_root=proj)
rc = controller.run_script("dummy.sh", env={"PROLE_HOME": "$HOME/prole"})
assert rc == 0

View File

@ -6,13 +6,11 @@ import os
from pathlib import Path
from unittest import mock
import pytest
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
from knoe.core.actions import KnoeConsoleInstaller
from knoe.core.controller import KnoeController
from knoe.milestone import Milestone
from knoe.state import InstallerState
# ===== InstallerState =====
@ -262,31 +260,31 @@ class TestBuildMilestone:
assert m.next(InstallerState()) is None
# ===== ProleController =====
# ===== KnoeController =====
class TestProleController:
class TestKnoeController:
def test_init(self, tmp_path):
c = ProleController(tmp_path)
c = KnoeController(tmp_path)
assert c.project_root == tmp_path
assert c.verbose is False
assert c.state is not None
installer = ProleConsoleInstaller(c)
installer = KnoeConsoleInstaller(c)
defaults = installer._default_inputs()
assert defaults.get("kerberos_config.user") == "administrator"
assert defaults.get("kerberos_config.password") == ""
def test_init_verbose(self, tmp_path):
c = ProleController(tmp_path, verbose=True)
c = KnoeController(tmp_path, verbose=True)
assert c.verbose is True
def test_check_docker_running(self, tmp_path):
c = ProleController(tmp_path)
c = KnoeController(tmp_path)
result = c.check_docker_running()
assert isinstance(result, bool)
def test_get_knoe_db_version_defaults(self, tmp_path):
c = ProleController(tmp_path)
c = KnoeController(tmp_path)
version = c.get_knoe_db_version()
assert "17.7" in version # default pg version
assert "-" in version
@ -298,12 +296,12 @@ class TestProleController:
db_dir = tmp_path / "knoe-db"
db_dir.mkdir()
(db_dir / ".version").write_text("42")
c = ProleController(tmp_path)
c = KnoeController(tmp_path)
version = c.get_knoe_db_version()
assert version == "16.2-042"
def test_run_milestones(self, tmp_path):
c = ProleController(tmp_path)
c = KnoeController(tmp_path)
m1 = _DummyMilestone("step1", "Step 1")
m2 = _DummyMilestone("step2", "Step 2")
msgs = []
@ -312,21 +310,21 @@ class TestProleController:
assert "step2" in c.state.completed
def test_run_milestones_no_callback(self, tmp_path):
c = ProleController(tmp_path)
c = KnoeController(tmp_path)
m = _DummyMilestone("s1", "S1")
c.run_milestones([m])
assert "s1" in c.state.completed
def test_run_script_missing(self, tmp_path):
c = ProleController(tmp_path)
c = KnoeController(tmp_path)
# Script doesn't exist but should not crash
rc = c.run_script("nonexistent_script.sh", args=[])
# Will fail since the script doesn't exist
assert rc != 0 or rc is None
def test_silent_installer_runs_supabase_preload_before_deploy(self, tmp_path):
c = ProleController(tmp_path)
installer = ProleConsoleInstaller(c)
c = KnoeController(tmp_path)
installer = KnoeConsoleInstaller(c)
with mock.patch.object(installer, "_load_inputs_from_cfg", return_value={}), \
mock.patch.object(installer, "_write_cfg"), \
@ -348,8 +346,8 @@ class TestProleController:
def test_silent_installer_does_not_generate_db_password_when_openbao_ref_present(
self, tmp_path
):
c = ProleController(tmp_path)
installer = ProleConsoleInstaller(c)
c = KnoeController(tmp_path)
installer = KnoeConsoleInstaller(c)
openbao_ref = "${OPENBAO:kv/prole/test/db#password}"

View File

@ -1,6 +1,7 @@
"""Tests for prole/deployment.py ProleDeployment class."""
"""Tests for prole/deployment.py KnoeDeployment class."""
from __future__ import annotations
import configparser
import os
import subprocess
@ -8,10 +9,8 @@ from pathlib import Path
from unittest import mock
from unittest.mock import MagicMock, patch, call
import pytest
from prole.deployment import (
ProleDeployment,
KnoeDeployment,
_CFG_KEY_MAP,
KUBECONFIG_FILENAME,
FETCH_PLAYBOOK,
@ -59,7 +58,7 @@ def _make_deployment(tmp_path, sections=None):
cfg_path = _make_cfg_file(tmp_path, sections)
ctrl = _make_controller(tmp_path)
with mock.patch.dict(os.environ, {"PROLE_CONF": str(tmp_path / "conf")}):
dep = ProleDeployment(ctrl, tmp_path)
dep = KnoeDeployment(ctrl, tmp_path)
return dep, ctrl
@ -68,7 +67,7 @@ def _make_deployment(tmp_path, sections=None):
# ---------------------------------------------------------------------------
class TestProleDeploymentInit:
class TestKnoeDeploymentInit:
def test_init_loads_cfg(self, tmp_path):
dep, _ = _make_deployment(tmp_path)
assert dep.prole_cfg_data["Global"]["NAMESPACE"] == "test-ns"
@ -86,14 +85,14 @@ class TestProleDeploymentInit:
cfg.write(f)
ctrl = _make_controller(tmp_path)
with mock.patch.dict(os.environ, {"PROLE_CONF": str(conf_dir)}):
dep = ProleDeployment(ctrl, tmp_path)
dep = KnoeDeployment(ctrl, tmp_path)
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)
dep = KnoeDeployment(ctrl, tmp_path)
assert dep.prole_cfg_data["Global"]["NAMESPACE"] == "knoe-db"
def test_load_cfg_from_directory(self, tmp_path):
@ -101,7 +100,7 @@ class TestProleDeploymentInit:
_make_cfg_file(tmp_path)
ctrl = _make_controller(tmp_path)
with mock.patch.dict(os.environ, {"PROLE_CONF": str(tmp_path / "conf")}):
dep = ProleDeployment(ctrl, tmp_path)
dep = KnoeDeployment(ctrl, tmp_path)
assert dep.cfg_path == tmp_path / "conf" / "prole.cfg"
@ -284,7 +283,7 @@ class TestApply:
dep.prole_cfg_data["Deployment"] = {"OPENTOFU_PIPELINE_DIR": str(pipeline)}
assert dep.apply() is False
@patch.object(ProleDeployment, "_run_post_apply_scripts", return_value=True)
@patch.object(KnoeDeployment, "_run_post_apply_scripts", return_value=True)
@patch("subprocess.run")
def test_apply_success(self, mock_run, mock_post, tmp_path):
pipeline = tmp_path / "pipeline"
@ -416,7 +415,7 @@ class TestRunSupabaseDeploy:
class TestDuplicateK3dToK3s:
@patch.object(ProleDeployment, "_fetch_kubeconfig", return_value=None)
@patch.object(KnoeDeployment, "_fetch_kubeconfig", return_value=None)
@patch(
"prole.deployment._detect_ansible_topology",
return_value={"k3s_server_url": "https://example.com:6443"},
@ -431,7 +430,7 @@ class TestDuplicateK3dToK3s:
assert dep.duplicate_k3d_to_k3s() is False
@patch("prole.deployment._sync_opentofu_pipeline", side_effect=Exception("boom"))
@patch.object(ProleDeployment, "_fetch_kubeconfig")
@patch.object(KnoeDeployment, "_fetch_kubeconfig")
@patch(
"prole.deployment._detect_ansible_topology",
return_value={
@ -447,7 +446,7 @@ class TestDuplicateK3dToK3s:
assert dep.duplicate_k3d_to_k3s() is False
@patch("prole.deployment._sync_opentofu_pipeline")
@patch.object(ProleDeployment, "_fetch_kubeconfig")
@patch.object(KnoeDeployment, "_fetch_kubeconfig")
@patch(
"prole.deployment._detect_ansible_topology",
return_value={

View File

@ -9,8 +9,33 @@ from pathlib import Path
from unittest import mock
import pytest
# ---------------------------------------------------------------------------
# config.py helpers
# ---------------------------------------------------------------------------
from knoe.config import (
_parse_bool as cfg_parse_bool,
_expand_path as cfg_expand_path,
_expand_cfg_value as cfg_expand_cfg_value,
_collect_cfg_vars as cfg_collect_cfg_vars,
_filter_cfg_values_for_persistence,
_normalize_cfg_value_for_persistence,
_extract_yaml_scalar_from_text as cfg_extract_yaml,
_extract_inline_vault_block as cfg_extract_vault,
_load_properties,
_write_k3s_kubeconfig,
_merge_kubeconfig,
normalize_version,
get_resource_path as cfg_get_resource_path,
get_docker_build_platform_args as cfg_get_docker_args,
_is_prole_secret as cfg_is_prole_secret,
_is_openbao_ref as cfg_is_openbao_ref,
_encrypt_cfg_secret,
_resolve_secret_value,
_update_prole_cfg_value,
get_properties,
get_config_value,
setup_logging,
)
# ---------------------------------------------------------------------------
# env.py helpers
# ---------------------------------------------------------------------------
@ -52,36 +77,6 @@ from knoe.core.env import (
_collect_cfg_vars as env_collect_cfg_vars,
get_resource_path,
is_apple_silicon,
get_docker_build_platform_args,
)
# ---------------------------------------------------------------------------
# config.py helpers
# ---------------------------------------------------------------------------
from knoe.config import (
_parse_bool as cfg_parse_bool,
_expand_path as cfg_expand_path,
_expand_cfg_value as cfg_expand_cfg_value,
_collect_cfg_vars as cfg_collect_cfg_vars,
_filter_cfg_values_for_persistence,
_normalize_cfg_value_for_persistence,
_extract_yaml_scalar_from_text as cfg_extract_yaml,
_extract_inline_vault_block as cfg_extract_vault,
_load_properties,
_write_k3s_kubeconfig,
_merge_kubeconfig,
normalize_version,
get_resource_path as cfg_get_resource_path,
is_apple_silicon as cfg_is_apple_silicon,
get_docker_build_platform_args as cfg_get_docker_args,
_is_prole_secret as cfg_is_prole_secret,
_is_openbao_ref as cfg_is_openbao_ref,
_encrypt_cfg_secret,
_resolve_secret_value,
_update_prole_cfg_value,
get_properties,
get_config_value,
setup_logging,
)
@ -894,7 +889,7 @@ class TestProperties:
# ===== _update_prole_cfg_value =====
class TestUpdateProleCfgValue:
class TestUpdateKnoeCfgValue:
def test_creates_section(self, tmp_path):
cfg_file = tmp_path / "conf" / "prole.cfg"
cfg_file.parent.mkdir(parents=True)
@ -1008,7 +1003,7 @@ class TestPlatformHelpers:
# ===== _render_prole_cfg =====
class TestRenderProleCfg:
class TestRenderKnoeCfg:
def test_minimal(self):
from knoe.core.env import _render_prole_cfg

View File

@ -3,8 +3,6 @@ from __future__ import annotations
import os
from pathlib import Path
import pytest
def test_environment_screen_expands_shell_vars_for_filesystem(monkeypatch, tmp_path: Path):
"""Regression: `$HOME`/`$PROLE_HOME` must be expanded before mkdir/copy.

View File

@ -1,9 +1,9 @@
"""Tests for the GitOps provider choice screen (_render_gitops_choice_page)."""
from __future__ import annotations
import sys
import tkinter as tk
from pathlib import Path
import sys
import pytest
PROJECT_ROOT = Path(__file__).resolve().parents[2]
if str(PROJECT_ROOT) not in sys.path:

View File

@ -11,7 +11,7 @@ from knoe.core.milestones import (
DeploymentMilestone,
)
from knoe.state import InstallerState
from knoe.core.controller import ProleController
from knoe.core.controller import KnoeController
class TestMilestones(unittest.TestCase):
@ -19,7 +19,7 @@ class TestMilestones(unittest.TestCase):
self.state = InstallerState()
self.state.inputs["dependencies.auto_install_missing"] = "True"
self.state.inputs["network_scan.run"] = "True"
self.state.controller = MagicMock(spec=ProleController)
self.state.controller = MagicMock(spec=KnoeController)
@patch("knoe.config.get_dep_info")
@patch("subprocess.run")

View File

@ -24,7 +24,7 @@ from knoe.core.milestones import (
InitializationScriptsMilestone,
)
from knoe.state import InstallerState
from knoe.core.controller import ProleController
from knoe.core.controller import KnoeController
# ---------------------------------------------------------------------------
@ -34,7 +34,7 @@ from knoe.core.controller import ProleController
def _make_state(**inputs) -> InstallerState:
state = InstallerState()
state.inputs.update(inputs)
state.controller = MagicMock(spec=ProleController)
state.controller = MagicMock(spec=KnoeController)
state.controller.project_root = Path("/tmp/fake-prole")
state.controller.get_knoe_db_version.return_value = "18-001"
state.controller.run_script.return_value = 0

View File

@ -10,7 +10,6 @@ from __future__ import annotations
import subprocess
from pathlib import Path
from unittest.mock import MagicMock, patch, call
import pytest
from knoe.core.monitor import (
_parse_pf_cfg,

View File

@ -1,14 +1,9 @@
from __future__ import annotations
from pathlib import Path
import pytest
from knoe.ui.screens.docker import DockerScreenMixin
import knoe.ui.screens.docker as docker_mod
from knoe.ui.screens.docker import DockerScreenMixin
class _DummyDockerScreen(DockerScreenMixin):

View File

@ -1,9 +1,11 @@
import sys
from unittest.mock import patch, MagicMock
from contextlib import ExitStack
import pytest
from pathlib import Path
import tkinter as tk
from contextlib import ExitStack
from pathlib import Path
from unittest.mock import patch, MagicMock
import pytest
class MockVar:
def __init__(self, value=None):
@ -25,8 +27,7 @@ sys.modules["PIL"] = MagicMock()
sys.modules["PIL.Image"] = MagicMock()
sys.modules["PIL.ImageTk"] = MagicMock()
import install
from install import ProleInstaller
from install import KnoeInstaller
def _generated_dockerfile_path(installer, tmp_path):
@ -43,8 +44,8 @@ def mock_installer(tmp_path):
patch("knoe.ui.screens.database_options.PROJECT_ROOT", tmp_path),
patch("knoe.ui.screens.database_options.resolve_prole_home", return_value=tmp_path),
patch("knoe.ui.screens.database_options.get_resource_path", side_effect=lambda name: tmp_path / name),
patch("knoe.core.controller.ProleController.run_script"),
patch.object(ProleInstaller, "_load_database_versions"),
patch("knoe.core.controller.KnoeController.run_script"),
patch.object(KnoeInstaller, "_load_database_versions"),
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"),
@ -67,7 +68,7 @@ def mock_installer(tmp_path):
(tmp_path / "conf" / "postgresql").mkdir(parents=True)
(tmp_path / "etc").mkdir()
app = ProleInstaller(root)
app = KnoeInstaller(root)
app.bg_canvas = MagicMock()
app.root = root
yield app

View File

@ -17,17 +17,16 @@ import pytest
from pathlib import Path
# Now import install.py after mocking
import install
from install import ProleInstaller
from install import KnoeInstaller
@pytest.fixture
def installer(tmp_path):
# Mocking os.environ to avoid messing with real env
with patch.dict(os.environ, {}, clear=True):
# We need to mock ProleInstaller.__init__ because it creates GUI elements
with patch.object(ProleInstaller, "__init__", return_value=None):
ins = ProleInstaller()
# We need to mock KnoeInstaller.__init__ because it creates GUI elements
with patch.object(KnoeInstaller, "__init__", return_value=None):
ins = KnoeInstaller()
# Core helpers expect `inputs` to exist; bypassing `__init__` means
# we must seed minimal state for method calls.
ins.inputs = {}

View File

@ -2,8 +2,8 @@ from __future__ import annotations
from pathlib import Path
from knoe.core.actions import ProleConsoleInstaller
from knoe.core.controller import ProleController
from knoe.core.actions import KnoeConsoleInstaller
from knoe.core.controller import KnoeController
def test_silent_installer_service_namespace_defaults_to_knoe_system_for_k3s(tmp_path, monkeypatch):
@ -11,8 +11,8 @@ def test_silent_installer_service_namespace_defaults_to_knoe_system_for_k3s(tmp_
monkeypatch.delenv("SERVICE_NAMESPACE", raising=False)
c = ProleController(tmp_path)
installer = ProleConsoleInstaller(c)
c = KnoeController(tmp_path)
installer = KnoeConsoleInstaller(c)
installer.inputs["init_cluster.cluster_env"] = "service"
installer.prole_cfg_data = {"Global": {}}

View File

@ -1,7 +1,8 @@
import sys
from unittest.mock import patch, MagicMock
import pytest
from pathlib import Path
from unittest.mock import patch, MagicMock
import pytest
# Mock tkinter and other GUI/macOS specific imports
sys.modules["tkinter"] = MagicMock()
@ -15,8 +16,7 @@ sys.modules["PIL"] = MagicMock()
sys.modules["PIL.Image"] = MagicMock()
sys.modules["PIL.ImageTk"] = MagicMock()
import install
from install import ProleInstaller
from install import KnoeInstaller
@pytest.fixture
@ -35,13 +35,13 @@ def mock_installer(tmp_path):
), patch(
"install.Path.mkdir"
), patch.object(
ProleInstaller, "_save_env_to_file"
KnoeInstaller, "_save_env_to_file"
), patch.object(
ProleInstaller, "_save_prole_cfg"
KnoeInstaller, "_save_prole_cfg"
), patch.object(
ProleInstaller, "_save_ansible_prole_vault"
KnoeInstaller, "_save_ansible_prole_vault"
), patch.object(
ProleInstaller, "_after_env_saved"
KnoeInstaller, "_after_env_saved"
), patch(
"threading.Thread"
):
@ -52,7 +52,7 @@ def mock_installer(tmp_path):
root.winfo_screenwidth.return_value = 1920
root.winfo_screenheight.return_value = 1080
app = ProleInstaller(root, config_path=str(cfg_path))
app = KnoeInstaller(root, config_path=str(cfg_path))
return app

View File

@ -5,13 +5,13 @@ from types import SimpleNamespace
import pytest
import knoe.core.actions as actions
from knoe.core.actions import ProleConsoleInstaller
from knoe.core.controller import ProleController
from knoe.core.actions import KnoeConsoleInstaller
from knoe.core.controller import KnoeController
def _mk_installer(tmp_path) -> ProleConsoleInstaller:
c = ProleController(tmp_path)
return ProleConsoleInstaller(c)
def _mk_installer(tmp_path) -> KnoeConsoleInstaller:
c = KnoeController(tmp_path)
return KnoeConsoleInstaller(c)
def test_repair_flow_classifies_blockers_attempts_reclaim_and_avoids_blind_health_wait(

View File

@ -6,8 +6,8 @@ import os
from types import SimpleNamespace
import knoe.core.actions as actions
from knoe.core.actions import ProleConsoleInstaller
from knoe.core.controller import ProleController
from knoe.core.actions import KnoeConsoleInstaller
from knoe.core.controller import KnoeController
@contextmanager
@ -30,9 +30,9 @@ def _preserve_namespace_env():
os.environ[k] = v
def _mk_installer(tmp_path) -> ProleConsoleInstaller:
c = ProleController(tmp_path)
return ProleConsoleInstaller(c)
def _mk_installer(tmp_path) -> KnoeConsoleInstaller:
c = KnoeController(tmp_path)
return KnoeConsoleInstaller(c)
def test_match_stale_released_pv_happy_path(tmp_path):

View File

@ -67,19 +67,19 @@ NAMESPACE = test-ns
def _make_installer(tmp_path: Path):
"""Return a minimal ProleConsoleInstaller with mocked controller."""
from knoe.core.actions import ProleConsoleInstaller
from knoe.core.controller import ProleController
"""Return a minimal KnoeConsoleInstaller with mocked controller."""
from knoe.core.actions import KnoeConsoleInstaller
from knoe.core.controller import KnoeController
cfg_path = tmp_path / "prole.cfg"
if not cfg_path.exists():
_write_cfg(cfg_path, "[Inputs]\n[Global]\n[Install]\nSTATUS = New\n")
controller = MagicMock(spec=ProleController)
controller = MagicMock(spec=KnoeController)
controller.project_root = tmp_path
controller.state = MagicMock()
installer = ProleConsoleInstaller.__new__(ProleConsoleInstaller)
installer = KnoeConsoleInstaller.__new__(KnoeConsoleInstaller)
installer.controller = controller
installer.cfg_path = cfg_path
installer.inputs = {}
@ -92,14 +92,14 @@ def _make_installer(tmp_path: Path):
def _defaults(installer):
"""Call _default_inputs() with all internal deps stubbed."""
from knoe.core.actions import ProleConsoleInstaller
from knoe.core.actions import KnoeConsoleInstaller
with (
patch.object(ProleConsoleInstaller, "_load_inputs_from_cfg", return_value={}),
patch.object(ProleConsoleInstaller, "_initial_db_namespace", return_value="test-ns"),
patch.object(ProleConsoleInstaller, "_cnpg_cluster_name", return_value="knoe-db"),
patch.object(ProleConsoleInstaller, "_get_local_owner", return_value="root"),
patch.object(ProleConsoleInstaller, "_env_defaults", return_value={}),
patch.object(KnoeConsoleInstaller, "_load_inputs_from_cfg", return_value={}),
patch.object(KnoeConsoleInstaller, "_initial_db_namespace", return_value="test-ns"),
patch.object(KnoeConsoleInstaller, "_cnpg_cluster_name", return_value="knoe-db"),
patch.object(KnoeConsoleInstaller, "_get_local_owner", return_value="root"),
patch.object(KnoeConsoleInstaller, "_env_defaults", return_value={}),
):
return installer._default_inputs()
@ -151,8 +151,8 @@ def test_main_update_flag_dispatches_to_run_update(tmp_path, monkeypatch):
monkeypatch.setattr(sys, "argv", ["install.py", "--update", "-c", str(cfg_path)])
monkeypatch.setattr(screens_mod, "_ensure_ansible_vault_credentials", lambda **_: None)
monkeypatch.setattr(screens_mod, "ProleController", MagicMock(return_value=MagicMock()))
monkeypatch.setattr(screens_mod, "ProleConsoleInstaller", MagicMock(return_value=installer))
monkeypatch.setattr(screens_mod, "KnoeController", MagicMock(return_value=MagicMock()))
monkeypatch.setattr(screens_mod, "KnoeConsoleInstaller", MagicMock(return_value=installer))
with pytest.raises(SystemExit) as ex:
screens_mod.main()
@ -198,7 +198,7 @@ def test_main_rejects_delete_db_without_reset(tmp_path, monkeypatch):
def test_main_delete_db_is_forwarded_to_console_installer(tmp_path, monkeypatch):
"""install.py --delete-db must be forwarded to ProleConsoleInstaller in silent mode."""
"""install.py --delete-db must be forwarded to KnoeConsoleInstaller in silent mode."""
import knoe.ui.screens as screens_mod
cfg_path = tmp_path / "prole.cfg"
@ -217,8 +217,8 @@ def test_main_delete_db_is_forwarded_to_console_installer(tmp_path, monkeypatch)
["install.py", "--silent", "--reset", "--delete-db", "-c", str(cfg_path)],
)
monkeypatch.setattr(screens_mod, "_ensure_ansible_vault_credentials", lambda **_: None)
monkeypatch.setattr(screens_mod, "ProleController", MagicMock(return_value=MagicMock()))
monkeypatch.setattr(screens_mod, "ProleConsoleInstaller", installer_factory)
monkeypatch.setattr(screens_mod, "KnoeController", MagicMock(return_value=MagicMock()))
monkeypatch.setattr(screens_mod, "KnoeConsoleInstaller", installer_factory)
with pytest.raises(SystemExit) as ex:
screens_mod.main()
@ -233,7 +233,7 @@ def test_main_delete_db_is_forwarded_to_console_installer(tmp_path, monkeypatch)
def test_repair_flag_does_not_trigger_silent_install(tmp_path):
"""--repair must dispatch to run_repair(), NOT run() (full install)."""
from knoe.core.actions import ProleConsoleInstaller
from knoe.core.actions import KnoeConsoleInstaller
installer = _make_installer(tmp_path)
@ -241,9 +241,9 @@ def test_repair_flag_does_not_trigger_silent_install(tmp_path):
run_repair_called = []
with (
patch.object(ProleConsoleInstaller, "run", side_effect=lambda: run_called.append(1) or 0),
patch.object(KnoeConsoleInstaller, "run", side_effect=lambda: run_called.append(1) or 0),
patch.object(
ProleConsoleInstaller,
KnoeConsoleInstaller,
"run_repair",
side_effect=lambda: run_repair_called.append(1) or 0,
),
@ -256,38 +256,38 @@ def test_repair_flag_does_not_trigger_silent_install(tmp_path):
def test_update_calls_repair_first(tmp_path):
"""run_update() must invoke run_repair() before applying config changes."""
from knoe.core.actions import ProleConsoleInstaller
from knoe.core.actions import KnoeConsoleInstaller
installer = _make_installer(tmp_path)
call_order: list[str] = []
with (
patch.object(
ProleConsoleInstaller,
KnoeConsoleInstaller,
"run_repair",
side_effect=lambda: call_order.append("repair") or 0,
),
patch.object(ProleConsoleInstaller, "_load_inputs_from_cfg", return_value={}),
patch.object(ProleConsoleInstaller, "_default_inputs", return_value={}),
patch.object(ProleConsoleInstaller, "_service_namespace", return_value="test-ns"),
patch.object(ProleConsoleInstaller, "_script_env_for_namespace", return_value={}),
patch.object(KnoeConsoleInstaller, "_load_inputs_from_cfg", return_value={}),
patch.object(KnoeConsoleInstaller, "_default_inputs", return_value={}),
patch.object(KnoeConsoleInstaller, "_service_namespace", return_value="test-ns"),
patch.object(KnoeConsoleInstaller, "_script_env_for_namespace", return_value={}),
patch.object(
ProleConsoleInstaller,
KnoeConsoleInstaller,
"_reconcile_supabase",
side_effect=lambda env: call_order.append("reconcile"),
),
patch.object(
ProleConsoleInstaller,
KnoeConsoleInstaller,
"_step_db_build",
side_effect=lambda: call_order.append("db_build"),
),
patch.object(
ProleConsoleInstaller,
KnoeConsoleInstaller,
"_step_cnpg_deploy",
side_effect=lambda: call_order.append("cnpg_deploy"),
),
patch.object(ProleConsoleInstaller, "_write_cfg", return_value=None),
patch.object(ProleConsoleInstaller, "_close_log_file", return_value=None),
patch.object(KnoeConsoleInstaller, "_write_cfg", return_value=None),
patch.object(KnoeConsoleInstaller, "_close_log_file", return_value=None),
):
installer.run_update()
@ -298,15 +298,15 @@ def test_update_calls_repair_first(tmp_path):
def test_update_forces_cnpg_rollout_only_for_deploy_step(tmp_path):
"""run_update() must force rollout for CNPG deploy, then restore configured value."""
from knoe.core.actions import ProleConsoleInstaller
from knoe.core.actions import KnoeConsoleInstaller
installer = _make_installer(tmp_path)
seen_force_rollout: list[bool] = []
with (
patch.object(ProleConsoleInstaller, "run_repair", return_value=0),
patch.object(KnoeConsoleInstaller, "run_repair", return_value=0),
patch.object(
ProleConsoleInstaller,
KnoeConsoleInstaller,
"_load_inputs_from_cfg",
return_value={
"init_cnpg_deploy.run_deploy": "true",
@ -315,13 +315,13 @@ def test_update_forces_cnpg_rollout_only_for_deploy_step(tmp_path):
"init_cluster.supabase_enabled": "false",
},
),
patch.object(ProleConsoleInstaller, "_default_inputs", return_value={}),
patch.object(ProleConsoleInstaller, "_service_namespace", return_value="test-ns"),
patch.object(ProleConsoleInstaller, "_script_env_for_namespace", return_value={}),
patch.object(ProleConsoleInstaller, "_write_cfg", return_value=None),
patch.object(ProleConsoleInstaller, "_close_log_file", return_value=None),
patch.object(KnoeConsoleInstaller, "_default_inputs", return_value={}),
patch.object(KnoeConsoleInstaller, "_service_namespace", return_value="test-ns"),
patch.object(KnoeConsoleInstaller, "_script_env_for_namespace", return_value={}),
patch.object(KnoeConsoleInstaller, "_write_cfg", return_value=None),
patch.object(KnoeConsoleInstaller, "_close_log_file", return_value=None),
patch.object(
ProleConsoleInstaller,
KnoeConsoleInstaller,
"_step_cnpg_deploy",
side_effect=lambda: seen_force_rollout.append(
installer._get_input_bool("init_cnpg_deploy.force_rollout", False)
@ -490,7 +490,7 @@ def test_render_supabase_component_flags_default_to_enabled(tmp_path, monkeypatc
def test_reconcile_supabase_skips_studio_when_disabled(tmp_path):
"""_reconcile_supabase() must skip Studio when supabase_studio_enabled=false."""
from knoe.core.actions import ProleConsoleInstaller
from knoe.core.actions import KnoeConsoleInstaller
installer = _make_installer(tmp_path)
base = _defaults(installer)
@ -502,7 +502,7 @@ def test_reconcile_supabase_skips_studio_when_disabled(tmp_path):
studio_reconcile_called = []
with patch.object(
ProleConsoleInstaller,
KnoeConsoleInstaller,
"_reconcile_supabase_studio",
side_effect=lambda env: studio_reconcile_called.append(1),
):
@ -515,7 +515,7 @@ def test_reconcile_supabase_skips_studio_when_disabled(tmp_path):
def test_reconcile_supabase_calls_studio_when_enabled(tmp_path):
"""_reconcile_supabase() must invoke Studio reconcile when enabled."""
from knoe.core.actions import ProleConsoleInstaller
from knoe.core.actions import KnoeConsoleInstaller
installer = _make_installer(tmp_path)
base = _defaults(installer)
@ -527,7 +527,7 @@ def test_reconcile_supabase_calls_studio_when_enabled(tmp_path):
studio_reconcile_called = []
with patch.object(
ProleConsoleInstaller,
KnoeConsoleInstaller,
"_reconcile_supabase_studio",
side_effect=lambda env: studio_reconcile_called.append(1),
):

View File

@ -1,8 +1,9 @@
import sys
from contextlib import ExitStack
from unittest.mock import patch, MagicMock
import pytest
from pathlib import Path
from unittest.mock import patch, MagicMock
import pytest
# Mock tkinter and other GUI/macOS specific imports
sys.modules["tkinter"] = MagicMock()
@ -15,8 +16,7 @@ sys.modules["PIL"] = MagicMock()
sys.modules["PIL.Image"] = MagicMock()
sys.modules["PIL.ImageTk"] = MagicMock()
import install
from install import ProleInstaller
from install import KnoeInstaller
@pytest.fixture
@ -38,10 +38,10 @@ def mock_installer(tmp_path):
stack.enter_context(patch("install.ttk.Style"))
stack.enter_context(patch("install.Path.exists", return_value=True))
stack.enter_context(patch("install.Path.mkdir"))
stack.enter_context(patch.object(ProleInstaller, "_save_env_to_file"))
stack.enter_context(patch.object(ProleInstaller, "_save_prole_cfg"))
stack.enter_context(patch.object(ProleInstaller, "_save_ansible_prole_vault"))
stack.enter_context(patch.object(ProleInstaller, "_after_env_saved"))
stack.enter_context(patch.object(KnoeInstaller, "_save_env_to_file"))
stack.enter_context(patch.object(KnoeInstaller, "_save_prole_cfg"))
stack.enter_context(patch.object(KnoeInstaller, "_save_ansible_prole_vault"))
stack.enter_context(patch.object(KnoeInstaller, "_after_env_saved"))
stack.enter_context(patch("threading.Thread"))
mock_entry.return_value.get.return_value.strip.return_value = "mock_val"
@ -49,7 +49,7 @@ def mock_installer(tmp_path):
root.winfo_screenwidth.return_value = 1920
root.winfo_screenheight.return_value = 1080
app = ProleInstaller(root, config_path=str(cfg_path))
app = KnoeInstaller(root, config_path=str(cfg_path))
try:
app.bg_canvas.winfo_width.return_value = 1000
except Exception: