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 ; 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. ; This file is used as input for Ansible deployment and k8s cluster creation.
[User] [User]
@ -13,10 +13,12 @@ build.run_build = false
database_options.distribution = percona database_options.distribution = percona
database_options.ext.dblink = true database_options.ext.dblink = true
database_options.ext.pg_buffercache = true database_options.ext.pg_buffercache = true
database_options.ext.pg_cron = true
database_options.ext.pg_freespacemap = true database_options.ext.pg_freespacemap = true
database_options.ext.pg_repack = true database_options.ext.pg_repack = true
database_options.ext.pg_stat_monitor = true database_options.ext.pg_stat_monitor = true
database_options.ext.pg_stat_statements = true database_options.ext.pg_stat_statements = true
database_options.ext.pg_tde = true
database_options.ext.pgaudit = true database_options.ext.pgaudit = true
database_options.ext.pgbadger = true database_options.ext.pgbadger = true
database_options.ext.pgcrypto = true database_options.ext.pgcrypto = true
@ -51,7 +53,7 @@ init_cluster.cluster_env = prod
init_cluster.deployment_target = prole-prod-cluster init_cluster.deployment_target = prole-prod-cluster
init_cluster.gitops_enabled = false init_cluster.gitops_enabled = false
init_cluster.k3s_server_url = https://myrddin.prole.org:6443 init_cluster.k3s_server_url = https://myrddin.prole.org:6443
init_cluster.k3s_token = ${PROLE_SECRET:v1: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.kerberos_enabled = true
init_cluster.mode = k8s init_cluster.mode = k8s
init_cluster.start_cluster = true init_cluster.start_cluster = true
@ -62,14 +64,14 @@ init_db_build.run_build = true
init_password.cluster_name = ${CLUSTER_NAME} init_password.cluster_name = ${CLUSTER_NAME}
init_password.db_host_port = 5432 init_password.db_host_port = 5432
init_password.db_namespace = ${DATABASE_NAMESPACE} init_password.db_namespace = ${DATABASE_NAMESPACE}
init_password.db_password = ${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:eN8L5Bo1KE2xGU-Z:y4ybETpw1POYjX2tP96lWTajMLFsiTXn5DNj7s_l6gBZInnGB5MjuxQTZ-G_FBnK} init_password.db_password_confirm = ${PROLE_SECRET:v1:uLCCn7fAxP1AHcv7:zahrhbz50ngH59bVmJUvbCm5OTM1Id_ksd5D61I-GixJETQVMewsr1oC7Avu8m6E}
init_password.db_username = root init_password.db_username = root
init_password.generate_ssh_key = true init_password.generate_ssh_key = true
init_scripts.run_scripts = true init_scripts.run_scripts = true
kerberos_config.enabled = true kerberos_config.enabled = true
kerberos_config.kdc = 10.0.0.3 kerberos_config.kdc = 10.0.0.3
kerberos_config.password = ${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.realm = PROLE.ORG
kerberos_config.test_connection = false kerberos_config.test_connection = false
kerberos_config.user = administrator kerberos_config.user = administrator
@ -77,25 +79,28 @@ network_scan.run = true
ollama_config.model = ollama_config.model =
ollama_config.server_host = fairyland.prole.org ollama_config.server_host = fairyland.prole.org
ollama_config.server_port = 11434 ollama_config.server_port = 11434
supabase_config.pv_base_dir = /synology/d005
supabase_config.pv_node = gandalf.prole.org
[Global] [Global]
; Variables used by name in more than one place or assumed global scope ; Variables used by name in more than one place or assumed global scope
ARGOCD_NAMESPACE = argocd ARGOCD_NAMESPACE = argocd
CLUSTER_ENV = prod CLUSTER_ENV = prod
CLUSTER_NAME = knoe-db 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_HOST_PORT = 5432
DB_PASSWORD = ${PROLE_SECRET:v1:hk7ryF9uC0HMNmE0:ZFtwyX85sqA61qZDuSLEL1gWwuC20r6qCKnTihsGvJ7PODH5XcIRSQ5Ms3Il8lMt} DB_PASSWORD = ${PROLE_SECRET:v1:7FWWyYsJ27QWE7Hf:E99YJFxLGoz_QyHPH7odXmCk6WBuYL3tWU0iBdpcHmb1LBph_VBAa5GyHmH6LVYP}
DEPLOYMENT_MODE = k8s DEPLOYMENT_MODE = k8s
DEPLOYMENT_TARGET = prole-prod-cluster DEPLOYMENT_TARGET = prole-prod-cluster
DOCKER_IMPORT_DIR =
DOCKER_PRELOAD = false DOCKER_PRELOAD = false
KNOE_DB_USER = root KNOE_DB_USER = root
OPTIONAL_WORKLOADS_MIN_READY_SCHEDULABLE_NODES = 2 OPTIONAL_WORKLOADS_MIN_READY_SCHEDULABLE_NODES = 2
PROLE_K3S_SERVER = https://myrddin.prole.org:6443 PROLE_K3S_SERVER = https://myrddin.prole.org:6443
PROLE_K3S_TOKEN = ${PROLE_SECRET:v1:eRF7V3BH7FObAF2t:omapQMsXQQA765nO3w8Y7LY5Hn4C1Mrglwk9xyNHEpQoKPBgrbd8aGZwtPE3UgmT_i5eeW110q0aWJtR4kwuK5q6LR9Ec2iHknIJYXA-v6isvhsPK8az1A8rE9kIVcahABT9mSEFmdNPk-UNvISsxqtOZ98fCXoFbfWegg==} PROLE_K3S_TOKEN = ${PROLE_SECRET:v1:dcAmcZGfYyHgaY_0:wtuc48pEhK9Ha7BC3XG57MCf_4Wqgs09g3Jam2VQIc_3jNMIEIxORsDxGJsqZtyN0zmdSxs1ApZos-h6h3R5AkEZsmqdw6TTcFpixP0RwuPWCe5ObWIMyPR3ztWcwDiM69fm1QiFUNGPvARdyw6dBgyRKRqDpVSfXMpM5w==}
PROLE_OPENTOFU_URL = http://127.0.0.1:8080
SERVICE_NAMESPACE = knoe-system SERVICE_NAMESPACE = knoe-system
SUPABASE_PV_BASE = /synology/d005
SUPABASE_PV_BASE_DIR = /synology/d005
SUPABASE_PV_NODE = gandalf.prole.org
[Welcome] [Welcome]
; No configuration values captured yet for this section. ; No configuration values captured yet for this section.
@ -110,7 +115,7 @@ ANSIBLE_DOMAIN = prole.org
ANSIBLE_INFRASTRUCTURE = /Users/chrisfu/dev/prole/infrastructure ANSIBLE_INFRASTRUCTURE = /Users/chrisfu/dev/prole/infrastructure
ANSIBLE_INVENTORY = /Users/chrisfu/dev/prole/infrastructure/inventory ANSIBLE_INVENTORY = /Users/chrisfu/dev/prole/infrastructure/inventory
ANSIBLE_REALM = PROLE.ORG 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_ANSIBLE_DETECTED = 10.0.0.3
KDC_AUTO_DETECTED = 10.0.0.3 KDC_AUTO_DETECTED = 10.0.0.3
KERBEROS_AUTO_ENABLED = True KERBEROS_AUTO_ENABLED = True
@ -133,7 +138,6 @@ PROLE_SERVICE = /Users/chrisfu/dev/prole/etc
[Ollama] [Ollama]
OLLAMA_HOST = http://fairyland.prole.org:11434 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_HOST = fairyland.prole.org
OLLAMA_SERVER_PORT = 11434 OLLAMA_SERVER_PORT = 11434
@ -152,23 +156,20 @@ GITOPS_ENABLED = False
[Dev Cluster (k3d)] [Dev Cluster (k3d)]
CLUSTER_ENV = dev CLUSTER_ENV = dev
DISPLAY_NAME = knoe-dev-cluster DISPLAY_NAME = knoe-dev-cluster
KUBECTL_CONTEXT =
MODE = k3d MODE = k3d
[Service Cluster (k3s)] [Service Cluster (k3s)]
CLUSTER_ENV = prole-service-cluster CLUSTER_ENV = prole-service-cluster
DISPLAY_NAME = prole-service-cluster DISPLAY_NAME = prole-service-cluster
K3S_SERVER_URL = https://myrddin.prole.org:6443 K3S_SERVER_URL = https://myrddin.prole.org:6443
K3S_TOKEN = ${PROLE_SECRET:v1:U7foZs_vBN4-2fLv:wFtVuuFMcJ-R9zXZwdOqxfPsTmRFZSkyvKc_Kqo_kL1-ie8MxWU2M25t78O7EjW9dFxEAYRsPanjxYKDHDd8arRW17kGIqtzlcRM9sXQJimTpa-mMgNCFew28b99c-LuCZbeG3nZU72joGZN6TZ5ZjZZbvJLAOA1Hq6b0A==} K3S_TOKEN = ${PROLE_SECRET:v1:ZBrRQReMetk0k4X5:yl6S40jqATR45Ivg5DxoGlo_32X7Q6Zz0BpKIzWO0iKpLe5xK4H-lYTxzucn6AsivxNrZTRjLsTiQ1CrtIcmtDJzOwPjobkt5t2XGKmcBrPoeoUjqSFJolp2-0ClawERDXTnqLHOQJhNLFpqxrRc_AiU0iHiT6DuuZZ9Kg==}
MODE = k3s MODE = k3s
PIPELINE_URL = http://127.0.0.1:8080
[Prod Cluster (k8s)] [Prod Cluster (k8s)]
ARTIFACTS_DIR = /Users/chrisfu/dev/prole/data/staging ARTIFACTS_DIR = /Users/chrisfu/dev/prole/data/staging
CLUSTER_ENV = prole-prod-cluster CLUSTER_ENV = prole-prod-cluster
DISPLAY_NAME = prole-prod-cluster DISPLAY_NAME = prole-prod-cluster
MODE = k8s MODE = k8s
PIPELINE_URL = http://127.0.0.1:8080
[Docker Build] [Docker Build]
; No configuration values captured yet for this section. ; No configuration values captured yet for this section.

View File

@ -1,5 +1,5 @@
; Prole Master Configuration File ; 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. ; This file is used as input for Ansible deployment and k8s cluster creation.
[User] [User]
@ -53,7 +53,7 @@ init_cluster.cluster_env = service
init_cluster.deployment_target = prole-service-cluster init_cluster.deployment_target = prole-service-cluster
init_cluster.gitops_enabled = false init_cluster.gitops_enabled = false
init_cluster.k3s_server_url = https://myrddin.prole.org:6443 init_cluster.k3s_server_url = https://myrddin.prole.org:6443
init_cluster.k3s_token = ${PROLE_SECRET:v1: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.kerberos_enabled = true
init_cluster.mode = k3s init_cluster.mode = k3s
init_cluster.start_cluster = true init_cluster.start_cluster = true
@ -64,14 +64,14 @@ init_db_build.run_build = true
init_password.cluster_name = ${CLUSTER_NAME} init_password.cluster_name = ${CLUSTER_NAME}
init_password.db_host_port = 5432 init_password.db_host_port = 5432
init_password.db_namespace = ${DATABASE_NAMESPACE} init_password.db_namespace = ${DATABASE_NAMESPACE}
init_password.db_password = ${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:4QVLv6L_Zb6w1Zja:4PHGYWVndekQb3ZG7hjNAoIQ_T2yEZlS} init_password.db_password_confirm = ${PROLE_SECRET:v1:OcWBTOiijqf-bfTT:6r-ioWdDRdkPNxmbEAQP2TYEPaycWasP}
init_password.db_username = root init_password.db_username = root
init_password.generate_ssh_key = true init_password.generate_ssh_key = true
init_scripts.run_scripts = true init_scripts.run_scripts = true
kerberos_config.enabled = true kerberos_config.enabled = true
kerberos_config.kdc = 10.0.0.3 kerberos_config.kdc = 10.0.0.3
kerberos_config.password = ${PROLE_SECRET:v1:lDmjQ4Cj45LYBD9_:9Aes8WNTZ97oYpi1aFvS8ODHDCQJJJCUBFgNrJBZ3a-Mgwp2} kerberos_config.password = ${PROLE_SECRET:v1:h9mzEWvGpnpU2FRz:5W8czWfjnRxzA8tJPk3eFRtrVLD5DxREkfqmQsnKLsbuAaec}
kerberos_config.realm = PROLE.ORG kerberos_config.realm = PROLE.ORG
kerberos_config.test_connection = false kerberos_config.test_connection = false
kerberos_config.user = administrator kerberos_config.user = administrator
@ -79,6 +79,8 @@ network_scan.run = true
ollama_config.model = ollama_config.model =
ollama_config.server_host = fairyland.prole.org ollama_config.server_host = fairyland.prole.org
ollama_config.server_port = 11434 ollama_config.server_port = 11434
supabase_config.pv_base_dir = /synology/d005
supabase_config.pv_node = gandalf.prole.org
[Global] [Global]
; Variables used by name in more than one place or assumed global scope ; Variables used by name in more than one place or assumed global scope
@ -92,7 +94,7 @@ CNPG_PLACEMENT_PLAN_ID = cnpg-placement-7036c5f65b6ef0db
CNPG_STAGE1_NODE = gandalf.prole.org CNPG_STAGE1_NODE = gandalf.prole.org
DATABASE_NAMESPACE = knoe-db DATABASE_NAMESPACE = knoe-db
DB_HOST_PORT = 5432 DB_HOST_PORT = 5432
DB_PASSWORD = ${PROLE_SECRET:v1:FgbkzVICzbA4WX7l:t5eAnQUYrTfAiXuUUPgZ8rBJVqAp-WFA} DB_PASSWORD = ${PROLE_SECRET:v1:M7Rt2Xp273X8ixb5:Rjys8A6SqmBv19DvPAvgihwJ0eJ8Xvda}
DEPLOYMENT_MODE = k3s DEPLOYMENT_MODE = k3s
DEPLOYMENT_TARGET = prole-service-cluster DEPLOYMENT_TARGET = prole-service-cluster
DOCKER_PRELOAD = false DOCKER_PRELOAD = false
@ -100,8 +102,11 @@ KNOE_DB_USER = root
KUBECONTEXT = knoe.dev.prole.org KUBECONTEXT = knoe.dev.prole.org
OPTIONAL_WORKLOADS_MIN_READY_SCHEDULABLE_NODES = 2 OPTIONAL_WORKLOADS_MIN_READY_SCHEDULABLE_NODES = 2
PROLE_K3S_SERVER = https://myrddin.prole.org:6443 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 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 SYNOLOGY_ROOTS = /synology/d001,/synology/d002,/synology/d003,/synology/d004,/synology/d005
[Welcome] [Welcome]
@ -133,7 +138,7 @@ PROLE_LOGS = /Users/chrisfu/dev/prole/logs
PROLE_SERVICE = /Users/chrisfu/dev/prole/etc PROLE_SERVICE = /Users/chrisfu/dev/prole/etc
[Monitoring] [Monitoring]
; No configuration values captured yet for this section. GRAFANA_ADMIN_PASSWORD = ${PROLE_SECRET:v1:st2uaIMwx55ftSPt:HjzyDuKpzGJyIDVzIKWD6YDzyoqZ4iJ9}
[Kerberos Authentication] [Kerberos Authentication]
; No configuration values captured yet for this section. ; No configuration values captured yet for this section.
@ -158,7 +163,7 @@ SUPABASE_ENABLED = False
[Initialize Cluster] [Initialize Cluster]
ENVIRONMENT = service ENVIRONMENT = service
K3S_SERVER_URL = https://myrddin.prole.org:6443 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)] [Dev Cluster (k3d)]
CLUSTER_ENV = dev CLUSTER_ENV = dev
@ -170,7 +175,7 @@ MODE = k3d
CLUSTER_ENV = prole-service-cluster CLUSTER_ENV = prole-service-cluster
DISPLAY_NAME = prole-service-cluster DISPLAY_NAME = prole-service-cluster
K3S_SERVER_URL = https://myrddin.prole.org:6443 K3S_SERVER_URL = https://myrddin.prole.org:6443
K3S_TOKEN = ${PROLE_SECRET:v1: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 MODE = k3s
[Prod Cluster (k8s)] [Prod Cluster (k8s)]
@ -183,7 +188,7 @@ MODE = k8s
; No configuration values captured yet for this section. ; No configuration values captured yet for this section.
[Initialization Scripts] [Initialization Scripts]
STATUS = Attempted STATUS = Completed
[Deployment] [Deployment]
MODE = k3s MODE = k3s

View File

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

View File

@ -2,10 +2,74 @@
<module version="4"> <module version="4">
<component name="AdditionalModuleElements"> <component name="AdditionalModuleElements">
<content url="file://$MODULE_DIR$" dumb="true"> <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$/.venv" />
<excludeFolder url="file://$MODULE_DIR$/deploy/gcp/terraform/.terraform" /> <excludeFolder url="file://$MODULE_DIR$/deploy/gcp/terraform/.terraform" />
<excludeFolder url="file://$MODULE_DIR$/deploy/opentofu/k3s/.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> </content>
</component> </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> </module>

View File

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

View File

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

View File

@ -9,13 +9,10 @@ import sys
import time import time
from pathlib import Path from pathlib import Path
from knoe.config import _expand_path, _collect_cfg_vars, _expand_cfg_value
from knoe import prole_conf from knoe import prole_conf
from knoe.config import _expand_path, _collect_cfg_vars, _expand_cfg_value
from knoe.core.env import ( from knoe.core.env import (
_build_required_port_forwards, _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 rollout(namespace, cluster_name, env, log) -> None
""" """
from __future__ import annotations from __future__ import annotations
import base64 import base64
import datetime import datetime
import json
import logging
import subprocess import subprocess
import time import time
from pathlib import Path from pathlib import Path
from typing import Callable
import json
import logging
from cryptography import x509 from cryptography import x509
from cryptography.hazmat.primitives import hashes, serialization from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric.ec import ( from cryptography.hazmat.primitives.asymmetric.ec import (
ECDSA,
SECP256R1, SECP256R1,
generate_private_key, generate_private_key,
) )
from cryptography.x509.oid import NameOID from cryptography.x509.oid import NameOID
from typing import Callable
_LOG = logging.getLogger(__name__) _LOG = logging.getLogger(__name__)
_LogFn = Callable[[str], None] _LogFn = Callable[[str], None]

View File

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

View File

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

View File

@ -1,7 +1,7 @@
""" """
Prole Service Dependencies Installer screen package. 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 mixin modules, one per logical screen / concern. The composed class is
assembled here and re-exported so that external callers (``install.py``, assembled here and re-exported so that external callers (``install.py``,
tests) see the same public surface as before. tests) see the same public surface as before.
@ -10,7 +10,6 @@ tests) see the same public surface as before.
from __future__ import annotations from __future__ import annotations
import configparser import configparser
import platform
import signal import signal
import socket import socket
import threading import threading
@ -18,45 +17,18 @@ import tkinter as tk
import uuid import uuid
from tkinter import ttk, messagebox, filedialog from tkinter import ttk, messagebox, filedialog
from knoe import deploy as inst_deploy import platform
from knoe import prole_conf
from knoe import screen as ui from knoe import screen as ui
from knoe.build import get_build_command as inst_get_build_command
from knoe.config import ( from knoe.config import (
_expand_cfg_value, _expand_cfg_value,
_collect_cfg_vars, _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 ( from knoe.core.env import (
_detect_ansible_topology,
_format_ansible_topology_summary,
_normalize_k3s_token,
_deployment_target_label, _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, _ensure_ansible_vault_credentials,
_openbao_placeholder,
_pf_upsert_mapping,
_default_opentofu_pipeline_url,
_build_required_port_forwards,
_render_prole_cfg,
_safe_str, _safe_str,
_normalize_cluster_env, _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. # 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.env import * # noqa: F401,F403
from knoe.core.actions import ( from knoe.core.actions import (
ProleInstaller, KnoeInstaller,
ProleConsoleInstaller, KnoeConsoleInstaller,
_prepare_k3s_pipeline, _prepare_k3s_pipeline,
_reset_k3s_namespace,
) )
# Keep global frames list in memory (legacy requirement). # Keep global frames list in memory (legacy requirement).
@ -108,8 +79,8 @@ from knoe.ui.screens.cfg import ConfigMixin
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Composed installer class # Composed installer class
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
class ProleInstaller( class KnoeInstaller(
ProleInstaller, KnoeInstaller,
ScreenBaseMixin, ScreenBaseMixin,
NavigationMixin, NavigationMixin,
WelcomeScreenMixin, WelcomeScreenMixin,
@ -212,7 +183,7 @@ class ProleInstaller(
conf_dir = prole_conf.resolve_prole_conf_dir(PROJECT_ROOT) conf_dir = prole_conf.resolve_prole_conf_dir(PROJECT_ROOT)
resolved_cfg = prole_conf.entrypoint_path(conf_dir) resolved_cfg = prole_conf.entrypoint_path(conf_dir)
self.controller = ProleController( self.controller = KnoeController(
PROJECT_ROOT, verbose=verbose, cfg_path=resolved_cfg PROJECT_ROOT, verbose=verbose, cfg_path=resolved_cfg
) )
self.project_root = PROJECT_ROOT self.project_root = PROJECT_ROOT
@ -734,6 +705,21 @@ class ProleInstaller(
self.kerberos_password = tk.StringVar() self.kerberos_password = tk.StringVar()
self.kerberos_kdc = tk.StringVar() self.kerberos_kdc = tk.StringVar()
self.supabase_enabled = tk.BooleanVar(value=False) 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.at_rest_encryption_enabled = tk.BooleanVar(value=True)
self.ollama_server_host = tk.StringVar() self.ollama_server_host = tk.StringVar()
self.ollama_server_port = tk.StringVar(value=DEFAULT_OLLAMA_PORT) self.ollama_server_port = tk.StringVar(value=DEFAULT_OLLAMA_PORT)
@ -891,6 +877,12 @@ def main():
action="store_true", action="store_true",
help="Reset k3d dev cluster or k3s service cluster", 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( parser.add_argument(
"--no-gui", "--no-gui",
action="store_true", action="store_true",
@ -919,6 +911,9 @@ def main():
parser.add_argument("command_args", nargs="*", help="Arguments for the subcommand") parser.add_argument("command_args", nargs="*", help="Arguments for the subcommand")
args = parser.parse_args() 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 # Set up log file tee if --log is requested
log_file_path = None log_file_path = None
if args.log is not None: if args.log is not None:
@ -933,7 +928,7 @@ def main():
setup_logging(verbose=args.verbose, debug=args.debug) setup_logging(verbose=args.verbose, debug=args.debug)
controller = ProleController( controller = KnoeController(
PROJECT_ROOT, PROJECT_ROOT,
verbose=args.verbose, verbose=args.verbose,
cfg_path=Path(args.config).expanduser() if args.config else None, cfg_path=Path(args.config).expanduser() if args.config else None,
@ -953,8 +948,11 @@ def main():
if args.silent: if args.silent:
_ensure_ansible_vault_credentials(prompt_ui=False) _ensure_ansible_vault_credentials(prompt_ui=False)
installer = ProleConsoleInstaller( installer = KnoeConsoleInstaller(
controller, args.config, reset_cluster=args.reset controller,
args.config,
reset_cluster=args.reset,
delete_db=args.delete_db,
) )
if log_file_path: if log_file_path:
installer.enable_log_file(log_file_path) installer.enable_log_file(log_file_path)
@ -994,7 +992,7 @@ def main():
except Exception: except Exception:
root.geometry(f"{window_width}x{window_height}") root.geometry(f"{window_width}x{window_height}")
_ensure_ansible_vault_credentials(prompt_ui=True, root=root) _ensure_ansible_vault_credentials(prompt_ui=True, root=root)
ProleInstaller( KnoeInstaller(
root, root,
config_path=args.config, config_path=args.config,
reset_cluster=args.reset, reset_cluster=args.reset,

View File

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

View File

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

View File

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

View File

@ -1,34 +1,18 @@
"""Cluster lifecycle management (k3d / k3s / k8s).""" """Cluster lifecycle management (k3d / k3s / k8s)."""
import json
import os import os
import re import re
import subprocess import subprocess
import sys import sys
import threading import threading
import time import time
import urllib.error
import urllib.request
from pathlib import Path
import tkinter as tk import tkinter as tk
from pathlib import Path
from tkinter import ttk, messagebox, filedialog 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 import prole_conf
from knoe.core.env import ( from knoe import screen as ui
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.config import ( from knoe.config import (
_collect_cfg_vars_from_data, _collect_cfg_vars_from_data,
_encrypt_cfg_secret, _encrypt_cfg_secret,
@ -37,6 +21,18 @@ from knoe.config import (
_write_k3s_kubeconfig, _write_k3s_kubeconfig,
) )
from knoe.core.actions import _reset_k3s_namespace 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 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_status_console = status_console
self._k3s_service_deploy_console = deploy_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.update_footer()
self._verify_k3s_services() self._verify_k3s_services()
@ -845,7 +844,7 @@ class ClusterScreenMixin:
return key return key
def _is_valid_k8s_namespace(self, value: str | None) -> bool: 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: if not ns or len(ns) > 63:
return False return False
return re.fullmatch(r"[a-z0-9]([-a-z0-9]*[a-z0-9])?", ns) is not None 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.""" """Database Options screen for selecting distribution, version and extensions."""
import json
import os import os
import re import re
import threading import threading
from pathlib import Path
import tkinter as tk import tkinter as tk
from pathlib import Path
from tkinter import ttk, messagebox from tkinter import ttk, messagebox
import json
from knoe import screen as ui from knoe import screen as ui
from knoe.core.build_context import copy_build_context_dir from knoe.core.build_context import copy_build_context_dir
from knoe.core.env import ( from knoe.core.env import (
@ -18,6 +18,7 @@ from knoe.core.env import (
resolve_prole_home, resolve_prole_home,
) )
class DatabaseOptionsScreenMixin: class DatabaseOptionsScreenMixin:
"""Mixin for the Database Options screen.""" """Mixin for the Database Options screen."""

View File

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

View File

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

View File

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

View File

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

View File

@ -63,6 +63,54 @@ class SupabaseScreenMixin:
self._overlay_widgets.append(enable_cb) self._overlay_widgets.append(enable_cb)
y = 280 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" initial_status = "Ready" if self.supabase_enabled.get() else "Disabled"
self._supabase_status_var = tk.StringVar(value=initial_status) self._supabase_status_var = tk.StringVar(value=initial_status)
status_item = ui.canvas_text( status_item = ui.canvas_text(
@ -86,10 +134,10 @@ class SupabaseScreenMixin:
# Standardized Console Output # Standardized Console Output
self._supabase_console = self._create_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 # Deploy Supabase Button
self._supabase_deploy_button = tk.Button( self._supabase_deploy_button = tk.Button(
@ -143,7 +191,27 @@ class SupabaseScreenMixin:
env["PROLE_SERVICE"] = str(PROJECT_ROOT) env["PROLE_SERVICE"] = str(PROJECT_ROOT)
env["NAMESPACE"] = namespace 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("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" script_path = PROJECT_ROOT / "supabase" / "deploy.sh"
if not script_path.exists(): if not script_path.exists():
@ -191,6 +259,12 @@ class SupabaseScreenMixin:
cfg_path = None cfg_path = None
if cfg_path: if cfg_path:
args.extend(["-c", str(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): if _parse_bool(os.environ.get("SUPABASE_USE_DEV_COMPOSE"), False):
args.append("--with-dev-helpers") args.append("--with-dev-helpers")
if _parse_bool(os.environ.get("SUPABASE_FOREGROUND"), False): if _parse_bool(os.environ.get("SUPABASE_FOREGROUND"), False):

View File

@ -3,10 +3,10 @@
import subprocess import subprocess
import threading import threading
import time import time
import webbrowser
import tkinter as tk import tkinter as tk
import webbrowser
from tkinter import ttk, messagebox, filedialog from tkinter import ttk, messagebox, filedialog
from knoe.screen import TerminalConsole
from knoe import screen as ui 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 from __future__ import annotations
import argparse import argparse
import logging
import os import os
import sys import sys
from pathlib import Path 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.env import _ensure_ansible_vault_credentials, PROJECT_ROOT
from knoe.core.actions import ProleConsoleInstaller, _prepare_k3s_pipeline
def main(): def main():
@ -44,6 +43,12 @@ def main():
action="store_true", action="store_true",
help="Reset k3d dev cluster or k3s service cluster", 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( parser.add_argument(
"--no-gui", "--no-gui",
action="store_true", action="store_true",
@ -63,6 +68,9 @@ def main():
prole_conf = os.environ.get("PROLE_CONF") prole_conf = os.environ.get("PROLE_CONF")
args = parser.parse_args() 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: if args.config is None and prole_conf:
potential_cfg = Path(prole_conf) / "prole.cfg" potential_cfg = Path(prole_conf) / "prole.cfg"
if potential_cfg.exists(): if potential_cfg.exists():
@ -72,16 +80,16 @@ def main():
setup_logging(verbose=args.verbose, debug=args.debug) setup_logging(verbose=args.verbose, debug=args.debug)
controller = ProleController( controller = KnoeController(
PROJECT_ROOT, PROJECT_ROOT,
verbose=args.verbose, verbose=args.verbose,
cfg_path=Path(args.config).expanduser() if args.config else None, cfg_path=Path(args.config).expanduser() if args.config else None,
) )
if args.command == "deploy": 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.duplicate_k3d_to_k3s():
if deployment.apply(): if deployment.apply():
sys.exit(0) sys.exit(0)
@ -89,8 +97,11 @@ def main():
if args.silent: if args.silent:
_ensure_ansible_vault_credentials(prompt_ui=False) _ensure_ansible_vault_credentials(prompt_ui=False)
installer = ProleConsoleInstaller( installer = KnoeConsoleInstaller(
controller, args.config, reset_cluster=args.reset controller,
args.config,
reset_cluster=args.reset,
delete_db=args.delete_db,
) )
if args.log: if args.log:
log_path = ( log_path = (

View File

@ -1,17 +1,16 @@
from __future__ import annotations from __future__ import annotations
import configparser import configparser
import logging
import os import os
import subprocess import subprocess
from pathlib import Path from pathlib import Path
import logging
from knoe import prole_conf from knoe import prole_conf
from knoe.core.actions import ProleInstaller from knoe.core.actions import KnoeInstaller
from knoe.core.env import ( from knoe.core.env import (
_detect_ansible_topology, _detect_ansible_topology,
_sync_opentofu_pipeline, _sync_opentofu_pipeline,
_normalize_cluster_env,
_parse_bool, _parse_bool,
) )
from knoe.core.ops.cloudnative_pg import initialize as cnpg_initialize 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. Top-level class for Prole infrastructure deployment via OpenTofu.
Duplicate k3d stack and apply to k3s defined in Ansible inventory. Duplicate k3d stack and apply to k3s defined in Ansible inventory.

View File

@ -1,58 +1,53 @@
Network Discovery Summary: Network Discovery Summary:
Primary Router: 10.0.0.1 (eero_5d:50:f2) 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: 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.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.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.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.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.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.6 [2c:cf:67:45:8d:97] (RaspberryPi_45:8d:97): Ports [22, 443], Services: ['SSH', 'Web Server']
- 10.0.0.33 [ec:b5:fa:b0:76:e4] (PhilipsLight_b0:76:e4): Ports [80, 443], Services: ['Web Server'] - 10.0.0.2 [dc:a4:ca:ea:1a:2f] (Apple_ea:1a:2f): Ports [], Services: []
- 10.0.0.34 [3a:b4:ce:79:b0:55] (3a:b4:ce:79:b0:55): 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.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.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.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.52 [2c:cf:67:ca:93:ce] (RaspberryPi_ca:93:ce): Ports [], Services: []
- 10.0.0.107 [b8:06:0d:b7:7c:56] (TuyaSmart_b7:7c:56): Ports [], Services: [] - 10.0.0.41 [b8:27:eb:57:10:d9] (RaspberryPiF_57:10:d9): 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.73 [4c:a9:19:b3:12:f8] (TuyaSmart_b3:12:f8): 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.100 [24:fc:e5:51:cf:74] (SamsungElect_51:cf:74): Ports [], Services: []
- 10.0.0.94 [f0:20:ff:bf:9a:19] (Intel_bf:9a:19): Ports [], Services: [] - 10.0.0.95 [b8:27:eb:88:3a:41] (RaspberryPiF_88:3a:41): Ports [2049], Services: ['NFS']
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.106 [48:a6:b8:a7:50:60] (Sonos_a7:50:60): Ports [], Services: [] - 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.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.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.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.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.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.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.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.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.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.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.188 [00:16:6c:c5:1f:54] (SamsungElect_c5:1f:54): Ports [80, 443], Services: ['Web Server']
- 10.0.0.199 [7c:a6:b0:01:a7:33] (7c:a6:b0:01:a7:33): Ports [], Services: [] - 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.60 [c6:d0:1a:97:9f:e9] (c6:d0:1a:97:9f:e9): Ports [], Services: [] - 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 Ollama Instances found at: 10.0.0.35, 10.0.0.204
Ansible Inventory Summary: Ansible Inventory Summary:

View File

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

View File

@ -67,6 +67,45 @@ def _is_placeholder(value: str) -> bool:
return v.startswith("${") and v.endswith("}") 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: def _discover_k8s_service_namespace(service_name: str) -> str:
try: try:
raw = subprocess.check_output(["kubectl", "get", "svc", "-A", "-o", "json"], text=True) 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) anon_key = _jwt(anon_payload, jwt_secret)
service_key = _jwt(service_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, "User", "supabase_hostname"),
_cfg_get(cfg, "Global", "supabase_hostname"), _cfg_get(cfg, "Global", "supabase_hostname"),
_cfg_get(cfg, "User", "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", default="db.prole.org",
).strip() ).strip()
if _is_placeholder(supabase_hostname): if _is_placeholder(supabase_hostname_raw):
supabase_hostname = "" supabase_hostname_raw = ""
if not supabase_hostname: if not supabase_hostname_raw:
supabase_hostname = "db.prole.org" 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). # Canonical public URL for Supabase (used for auth callbacks + Studio links).
public_url = "" public_url = ""
ingress_host = supabase_hostname primary_raw_host = (raw_host_entries[0] or "").strip()
if "://" in supabase_hostname: if "://" in primary_raw_host:
parsed = urllib.parse.urlparse(supabase_hostname) parsed = urllib.parse.urlparse(primary_raw_host)
ingress_host = (parsed.hostname or "").strip() or ingress_host ingress_host = (parsed.hostname or "").strip() or ingress_host
scheme = (parsed.scheme or "https").strip() scheme = (parsed.scheme or "https").strip()
if parsed.netloc: if parsed.netloc:
@ -220,9 +318,13 @@ def _build_overlay(cfg: configparser.ConfigParser, args: argparse.Namespace) ->
).strip().lower() ).strip().lower()
supabase_primary_node = _first( supabase_primary_node = _first(
os.environ.get("SUPABASE_PRIMARY_NODE", ""), 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_PRIMARY_NODE"),
_cfg_get(cfg, "Global", "SUPABASE_NODE_SELECTOR"),
_cfg_get(cfg, "Global", "SUPABASE_PV_NODE"),
_cfg_get(cfg, "Supabase", "PRIMARY_NODE"), _cfg_get(cfg, "Supabase", "PRIMARY_NODE"),
default="merlin.prole.org", default="gandalf.prole.org",
).strip() ).strip()
if _is_placeholder(supabase_primary_node): if _is_placeholder(supabase_primary_node):
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", ""), os.environ.get("SUPABASE_STORAGE_CLASS", ""),
_cfg_get(cfg, "Global", "SUPABASE_STORAGE_CLASS"), _cfg_get(cfg, "Global", "SUPABASE_STORAGE_CLASS"),
_cfg_get(cfg, "Supabase", "STORAGE_CLASS"), _cfg_get(cfg, "Supabase", "STORAGE_CLASS"),
default="merlin-local-iscsi-d002", default="synology-iscsi",
).strip() ).strip()
if _is_placeholder(supabase_storage_class): if _is_placeholder(supabase_storage_class):
supabase_storage_class = "merlin-local-iscsi-d002" supabase_storage_class = "merlin-local-iscsi-d002"
@ -243,20 +345,20 @@ def _build_overlay(cfg: configparser.ConfigParser, args: argparse.Namespace) ->
"fullnameOverride": "supabase", "fullnameOverride": "supabase",
"deployment": { "deployment": {
"db": {"enabled": False}, "db": {"enabled": False},
"functions": {"enabled": False}, "functions": {"enabled": True},
"vector": {"enabled": False}, "vector": {"enabled": True},
"kong": {"enabled": False}, "kong": {"enabled": True},
"storage": {"enabled": False}, "storage": {"enabled": True},
"minio": {"enabled": False}, "minio": {"enabled": True},
"imgproxy": {"enabled": False}, "imgproxy": {"enabled": True},
# Explicit fullnameOverride per component strips the chart name # Explicit fullnameOverride per component strips the chart name
# from pod names (avoids 'supabase-prole-supabase-<component>'). # from pod names (avoids 'supabase-prole-supabase-<component>').
"analytics": {"fullnameOverride": "supabase-analytics"}, "analytics": {"enabled": analytics_enabled, "fullnameOverride": "supabase-analytics"},
"auth": {"fullnameOverride": "supabase-auth"}, "auth": {"enabled": auth_enabled, "fullnameOverride": "supabase-auth"},
"meta": {"fullnameOverride": "supabase-meta"}, "meta": {"enabled": meta_enabled, "fullnameOverride": "supabase-meta"},
"realtime": {"fullnameOverride": "supabase-realtime"}, "realtime": {"enabled": realtime_enabled, "fullnameOverride": "supabase-realtime"},
"rest": {"fullnameOverride": "supabase-rest"}, "rest": {"enabled": True, "fullnameOverride": "supabase-rest"},
"studio": {"fullnameOverride": "supabase-studio"}, "studio": {"enabled": studio_enabled, "fullnameOverride": "supabase-studio"},
}, },
"externalDatabase": { "externalDatabase": {
"enabled": True, "enabled": True,
@ -287,9 +389,12 @@ def _build_overlay(cfg: configparser.ConfigParser, args: argparse.Namespace) ->
"ingress": { "ingress": {
"enabled": True, "enabled": True,
"className": "traefik", "className": "traefik",
"hosts": [ "hosts": [{"host": host, "paths": [{"path": "/", "pathType": "Prefix"}]} for host in host_entries],
{"host": ingress_host, "paths": [{"path": "/", "pathType": "Prefix"}]} },
], "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 # Write to persistence.*.storageClassName — the path the Helm chart actually uses
persistence = overlay.setdefault("persistence", {}) 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", "imgproxy", "minio", "storage"):
for pvc_key in ("functions", "snippets", "deno"):
persistence.setdefault(pvc_key, {})["storageClassName"] = supabase_storage_class persistence.setdefault(pvc_key, {})["storageClassName"] = supabase_storage_class
if mode == "k3s" and supabase_node_selector: if mode == "k3s" and supabase_node_selector:
deployment = overlay.setdefault("deployment", {}) deployment = overlay.setdefault("deployment", {})
# Only pin active control-plane components; storage/minio/imgproxy/kong are disabled.
for component in ( for component in (
"analytics", "analytics",
"auth", "auth",
"functions",
"imgproxy",
"kong",
"meta", "meta",
"minio",
"realtime", "realtime",
"rest", "rest",
"storage",
"studio", "studio",
"vector",
): ):
component_cfg = deployment.setdefault(component, {}) component_cfg = deployment.setdefault(component, {})
component_cfg.setdefault("nodeSelector", {}) 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") (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): def __init__(self, project_root: Path):
self.project_root = project_root self.project_root = project_root
self.inputs = {"init_cluster.cluster_env": "dev"} self.inputs = {"init_cluster.cluster_env": "dev"}

View File

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

View File

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

View File

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

View File

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

View File

@ -1,7 +1,7 @@
""" """
Unit tests for installer/core/controller.py 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 from __future__ import annotations
@ -9,17 +9,16 @@ import os
import subprocess import subprocess
from pathlib import Path from pathlib import Path
from unittest.mock import MagicMock, patch from unittest.mock import MagicMock, patch
import pytest
from knoe.core.controller import ProleController from knoe.core.controller import KnoeController
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Helpers # Helpers
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
def _make_controller(tmp_path: Path, **kwargs) -> ProleController: def _make_controller(tmp_path: Path, **kwargs) -> KnoeController:
return ProleController(project_root=tmp_path, **kwargs) return KnoeController(project_root=tmp_path, **kwargs)
def _scaffold_project(tmp_path: Path) -> None: 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").mkdir(parents=True, exist_ok=True)
(proj / "etc" / "dummy.sh").write_text("#!/usr/bin/env bash\necho ok\n") (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"}) rc = controller.run_script("dummy.sh", env={"PROLE_HOME": "$HOME/prole"})
assert rc == 0 assert rc == 0

View File

@ -6,13 +6,11 @@ import os
from pathlib import Path from pathlib import Path
from unittest import mock 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.build import get_build_command, BuildMilestone
from knoe.core.controller import ProleController from knoe.core.actions import KnoeConsoleInstaller
from knoe.core.actions import ProleConsoleInstaller from knoe.core.controller import KnoeController
from knoe.milestone import Milestone
from knoe.state import InstallerState
# ===== InstallerState ===== # ===== InstallerState =====
@ -262,31 +260,31 @@ class TestBuildMilestone:
assert m.next(InstallerState()) is None assert m.next(InstallerState()) is None
# ===== ProleController ===== # ===== KnoeController =====
class TestProleController: class TestKnoeController:
def test_init(self, tmp_path): def test_init(self, tmp_path):
c = ProleController(tmp_path) c = KnoeController(tmp_path)
assert c.project_root == tmp_path assert c.project_root == tmp_path
assert c.verbose is False assert c.verbose is False
assert c.state is not None assert c.state is not None
installer = ProleConsoleInstaller(c) installer = KnoeConsoleInstaller(c)
defaults = installer._default_inputs() defaults = installer._default_inputs()
assert defaults.get("kerberos_config.user") == "administrator" assert defaults.get("kerberos_config.user") == "administrator"
assert defaults.get("kerberos_config.password") == "" assert defaults.get("kerberos_config.password") == ""
def test_init_verbose(self, tmp_path): def test_init_verbose(self, tmp_path):
c = ProleController(tmp_path, verbose=True) c = KnoeController(tmp_path, verbose=True)
assert c.verbose is True assert c.verbose is True
def test_check_docker_running(self, tmp_path): def test_check_docker_running(self, tmp_path):
c = ProleController(tmp_path) c = KnoeController(tmp_path)
result = c.check_docker_running() result = c.check_docker_running()
assert isinstance(result, bool) assert isinstance(result, bool)
def test_get_knoe_db_version_defaults(self, tmp_path): def test_get_knoe_db_version_defaults(self, tmp_path):
c = ProleController(tmp_path) c = KnoeController(tmp_path)
version = c.get_knoe_db_version() version = c.get_knoe_db_version()
assert "17.7" in version # default pg version assert "17.7" in version # default pg version
assert "-" in version assert "-" in version
@ -298,12 +296,12 @@ class TestProleController:
db_dir = tmp_path / "knoe-db" db_dir = tmp_path / "knoe-db"
db_dir.mkdir() db_dir.mkdir()
(db_dir / ".version").write_text("42") (db_dir / ".version").write_text("42")
c = ProleController(tmp_path) c = KnoeController(tmp_path)
version = c.get_knoe_db_version() version = c.get_knoe_db_version()
assert version == "16.2-042" assert version == "16.2-042"
def test_run_milestones(self, tmp_path): def test_run_milestones(self, tmp_path):
c = ProleController(tmp_path) c = KnoeController(tmp_path)
m1 = _DummyMilestone("step1", "Step 1") m1 = _DummyMilestone("step1", "Step 1")
m2 = _DummyMilestone("step2", "Step 2") m2 = _DummyMilestone("step2", "Step 2")
msgs = [] msgs = []
@ -312,21 +310,21 @@ class TestProleController:
assert "step2" in c.state.completed assert "step2" in c.state.completed
def test_run_milestones_no_callback(self, tmp_path): def test_run_milestones_no_callback(self, tmp_path):
c = ProleController(tmp_path) c = KnoeController(tmp_path)
m = _DummyMilestone("s1", "S1") m = _DummyMilestone("s1", "S1")
c.run_milestones([m]) c.run_milestones([m])
assert "s1" in c.state.completed assert "s1" in c.state.completed
def test_run_script_missing(self, tmp_path): def test_run_script_missing(self, tmp_path):
c = ProleController(tmp_path) c = KnoeController(tmp_path)
# Script doesn't exist but should not crash # Script doesn't exist but should not crash
rc = c.run_script("nonexistent_script.sh", args=[]) rc = c.run_script("nonexistent_script.sh", args=[])
# Will fail since the script doesn't exist # Will fail since the script doesn't exist
assert rc != 0 or rc is None assert rc != 0 or rc is None
def test_silent_installer_runs_supabase_preload_before_deploy(self, tmp_path): def test_silent_installer_runs_supabase_preload_before_deploy(self, tmp_path):
c = ProleController(tmp_path) c = KnoeController(tmp_path)
installer = ProleConsoleInstaller(c) installer = KnoeConsoleInstaller(c)
with mock.patch.object(installer, "_load_inputs_from_cfg", return_value={}), \ with mock.patch.object(installer, "_load_inputs_from_cfg", return_value={}), \
mock.patch.object(installer, "_write_cfg"), \ 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( def test_silent_installer_does_not_generate_db_password_when_openbao_ref_present(
self, tmp_path self, tmp_path
): ):
c = ProleController(tmp_path) c = KnoeController(tmp_path)
installer = ProleConsoleInstaller(c) installer = KnoeConsoleInstaller(c)
openbao_ref = "${OPENBAO:kv/prole/test/db#password}" 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 from __future__ import annotations
import configparser import configparser
import os import os
import subprocess import subprocess
@ -8,10 +9,8 @@ from pathlib import Path
from unittest import mock from unittest import mock
from unittest.mock import MagicMock, patch, call from unittest.mock import MagicMock, patch, call
import pytest
from prole.deployment import ( from prole.deployment import (
ProleDeployment, KnoeDeployment,
_CFG_KEY_MAP, _CFG_KEY_MAP,
KUBECONFIG_FILENAME, KUBECONFIG_FILENAME,
FETCH_PLAYBOOK, FETCH_PLAYBOOK,
@ -59,7 +58,7 @@ def _make_deployment(tmp_path, sections=None):
cfg_path = _make_cfg_file(tmp_path, sections) cfg_path = _make_cfg_file(tmp_path, sections)
ctrl = _make_controller(tmp_path) ctrl = _make_controller(tmp_path)
with mock.patch.dict(os.environ, {"PROLE_CONF": str(tmp_path / "conf")}): 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 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): def test_init_loads_cfg(self, tmp_path):
dep, _ = _make_deployment(tmp_path) dep, _ = _make_deployment(tmp_path)
assert dep.prole_cfg_data["Global"]["NAMESPACE"] == "test-ns" assert dep.prole_cfg_data["Global"]["NAMESPACE"] == "test-ns"
@ -86,14 +85,14 @@ class TestProleDeploymentInit:
cfg.write(f) cfg.write(f)
ctrl = _make_controller(tmp_path) ctrl = _make_controller(tmp_path)
with mock.patch.dict(os.environ, {"PROLE_CONF": str(conf_dir)}): 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" assert dep.prole_cfg_data["Global"]["NAMESPACE"] == "knoe-db"
def test_init_no_cfg_file(self, tmp_path): def test_init_no_cfg_file(self, tmp_path):
"""When no prole.cfg exists, it still initialises.""" """When no prole.cfg exists, it still initialises."""
ctrl = _make_controller(tmp_path) ctrl = _make_controller(tmp_path)
with mock.patch.dict(os.environ, {"PROLE_CONF": ""}, clear=False): 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" assert dep.prole_cfg_data["Global"]["NAMESPACE"] == "knoe-db"
def test_load_cfg_from_directory(self, tmp_path): def test_load_cfg_from_directory(self, tmp_path):
@ -101,7 +100,7 @@ class TestProleDeploymentInit:
_make_cfg_file(tmp_path) _make_cfg_file(tmp_path)
ctrl = _make_controller(tmp_path) ctrl = _make_controller(tmp_path)
with mock.patch.dict(os.environ, {"PROLE_CONF": str(tmp_path / "conf")}): 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" 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)} dep.prole_cfg_data["Deployment"] = {"OPENTOFU_PIPELINE_DIR": str(pipeline)}
assert dep.apply() is False 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") @patch("subprocess.run")
def test_apply_success(self, mock_run, mock_post, tmp_path): def test_apply_success(self, mock_run, mock_post, tmp_path):
pipeline = tmp_path / "pipeline" pipeline = tmp_path / "pipeline"
@ -416,7 +415,7 @@ class TestRunSupabaseDeploy:
class TestDuplicateK3dToK3s: class TestDuplicateK3dToK3s:
@patch.object(ProleDeployment, "_fetch_kubeconfig", return_value=None) @patch.object(KnoeDeployment, "_fetch_kubeconfig", return_value=None)
@patch( @patch(
"prole.deployment._detect_ansible_topology", "prole.deployment._detect_ansible_topology",
return_value={"k3s_server_url": "https://example.com:6443"}, return_value={"k3s_server_url": "https://example.com:6443"},
@ -431,7 +430,7 @@ class TestDuplicateK3dToK3s:
assert dep.duplicate_k3d_to_k3s() is False assert dep.duplicate_k3d_to_k3s() is False
@patch("prole.deployment._sync_opentofu_pipeline", side_effect=Exception("boom")) @patch("prole.deployment._sync_opentofu_pipeline", side_effect=Exception("boom"))
@patch.object(ProleDeployment, "_fetch_kubeconfig") @patch.object(KnoeDeployment, "_fetch_kubeconfig")
@patch( @patch(
"prole.deployment._detect_ansible_topology", "prole.deployment._detect_ansible_topology",
return_value={ return_value={
@ -447,7 +446,7 @@ class TestDuplicateK3dToK3s:
assert dep.duplicate_k3d_to_k3s() is False assert dep.duplicate_k3d_to_k3s() is False
@patch("prole.deployment._sync_opentofu_pipeline") @patch("prole.deployment._sync_opentofu_pipeline")
@patch.object(ProleDeployment, "_fetch_kubeconfig") @patch.object(KnoeDeployment, "_fetch_kubeconfig")
@patch( @patch(
"prole.deployment._detect_ansible_topology", "prole.deployment._detect_ansible_topology",
return_value={ return_value={

View File

@ -9,8 +9,33 @@ from pathlib import Path
from unittest import mock from unittest import mock
import pytest 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 # env.py helpers
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@ -52,36 +77,6 @@ from knoe.core.env import (
_collect_cfg_vars as env_collect_cfg_vars, _collect_cfg_vars as env_collect_cfg_vars,
get_resource_path, get_resource_path,
is_apple_silicon, 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 ===== # ===== _update_prole_cfg_value =====
class TestUpdateProleCfgValue: class TestUpdateKnoeCfgValue:
def test_creates_section(self, tmp_path): def test_creates_section(self, tmp_path):
cfg_file = tmp_path / "conf" / "prole.cfg" cfg_file = tmp_path / "conf" / "prole.cfg"
cfg_file.parent.mkdir(parents=True) cfg_file.parent.mkdir(parents=True)
@ -1008,7 +1003,7 @@ class TestPlatformHelpers:
# ===== _render_prole_cfg ===== # ===== _render_prole_cfg =====
class TestRenderProleCfg: class TestRenderKnoeCfg:
def test_minimal(self): def test_minimal(self):
from knoe.core.env import _render_prole_cfg from knoe.core.env import _render_prole_cfg

View File

@ -3,8 +3,6 @@ from __future__ import annotations
import os import os
from pathlib import Path from pathlib import Path
import pytest
def test_environment_screen_expands_shell_vars_for_filesystem(monkeypatch, tmp_path: Path): def test_environment_screen_expands_shell_vars_for_filesystem(monkeypatch, tmp_path: Path):
"""Regression: `$HOME`/`$PROLE_HOME` must be expanded before mkdir/copy. """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).""" """Tests for the GitOps provider choice screen (_render_gitops_choice_page)."""
from __future__ import annotations from __future__ import annotations
import sys
import tkinter as tk import tkinter as tk
from pathlib import Path from pathlib import Path
import sys
import pytest
PROJECT_ROOT = Path(__file__).resolve().parents[2] PROJECT_ROOT = Path(__file__).resolve().parents[2]
if str(PROJECT_ROOT) not in sys.path: if str(PROJECT_ROOT) not in sys.path:

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -5,13 +5,13 @@ from types import SimpleNamespace
import pytest import pytest
import knoe.core.actions as actions import knoe.core.actions as actions
from knoe.core.actions import ProleConsoleInstaller from knoe.core.actions import KnoeConsoleInstaller
from knoe.core.controller import ProleController from knoe.core.controller import KnoeController
def _mk_installer(tmp_path) -> ProleConsoleInstaller: def _mk_installer(tmp_path) -> KnoeConsoleInstaller:
c = ProleController(tmp_path) c = KnoeController(tmp_path)
return ProleConsoleInstaller(c) return KnoeConsoleInstaller(c)
def test_repair_flow_classifies_blockers_attempts_reclaim_and_avoids_blind_health_wait( 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 from types import SimpleNamespace
import knoe.core.actions as actions import knoe.core.actions as actions
from knoe.core.actions import ProleConsoleInstaller from knoe.core.actions import KnoeConsoleInstaller
from knoe.core.controller import ProleController from knoe.core.controller import KnoeController
@contextmanager @contextmanager
@ -30,9 +30,9 @@ def _preserve_namespace_env():
os.environ[k] = v os.environ[k] = v
def _mk_installer(tmp_path) -> ProleConsoleInstaller: def _mk_installer(tmp_path) -> KnoeConsoleInstaller:
c = ProleController(tmp_path) c = KnoeController(tmp_path)
return ProleConsoleInstaller(c) return KnoeConsoleInstaller(c)
def test_match_stale_released_pv_happy_path(tmp_path): 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): def _make_installer(tmp_path: Path):
"""Return a minimal ProleConsoleInstaller with mocked controller.""" """Return a minimal KnoeConsoleInstaller with mocked controller."""
from knoe.core.actions import ProleConsoleInstaller from knoe.core.actions import KnoeConsoleInstaller
from knoe.core.controller import ProleController from knoe.core.controller import KnoeController
cfg_path = tmp_path / "prole.cfg" cfg_path = tmp_path / "prole.cfg"
if not cfg_path.exists(): if not cfg_path.exists():
_write_cfg(cfg_path, "[Inputs]\n[Global]\n[Install]\nSTATUS = New\n") _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.project_root = tmp_path
controller.state = MagicMock() controller.state = MagicMock()
installer = ProleConsoleInstaller.__new__(ProleConsoleInstaller) installer = KnoeConsoleInstaller.__new__(KnoeConsoleInstaller)
installer.controller = controller installer.controller = controller
installer.cfg_path = cfg_path installer.cfg_path = cfg_path
installer.inputs = {} installer.inputs = {}
@ -92,14 +92,14 @@ def _make_installer(tmp_path: Path):
def _defaults(installer): def _defaults(installer):
"""Call _default_inputs() with all internal deps stubbed.""" """Call _default_inputs() with all internal deps stubbed."""
from knoe.core.actions import ProleConsoleInstaller from knoe.core.actions import KnoeConsoleInstaller
with ( with (
patch.object(ProleConsoleInstaller, "_load_inputs_from_cfg", return_value={}), patch.object(KnoeConsoleInstaller, "_load_inputs_from_cfg", return_value={}),
patch.object(ProleConsoleInstaller, "_initial_db_namespace", return_value="test-ns"), patch.object(KnoeConsoleInstaller, "_initial_db_namespace", return_value="test-ns"),
patch.object(ProleConsoleInstaller, "_cnpg_cluster_name", return_value="knoe-db"), patch.object(KnoeConsoleInstaller, "_cnpg_cluster_name", return_value="knoe-db"),
patch.object(ProleConsoleInstaller, "_get_local_owner", return_value="root"), patch.object(KnoeConsoleInstaller, "_get_local_owner", return_value="root"),
patch.object(ProleConsoleInstaller, "_env_defaults", return_value={}), patch.object(KnoeConsoleInstaller, "_env_defaults", return_value={}),
): ):
return installer._default_inputs() 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(sys, "argv", ["install.py", "--update", "-c", str(cfg_path)])
monkeypatch.setattr(screens_mod, "_ensure_ansible_vault_credentials", lambda **_: None) monkeypatch.setattr(screens_mod, "_ensure_ansible_vault_credentials", lambda **_: None)
monkeypatch.setattr(screens_mod, "ProleController", MagicMock(return_value=MagicMock())) monkeypatch.setattr(screens_mod, "KnoeController", MagicMock(return_value=MagicMock()))
monkeypatch.setattr(screens_mod, "ProleConsoleInstaller", MagicMock(return_value=installer)) monkeypatch.setattr(screens_mod, "KnoeConsoleInstaller", MagicMock(return_value=installer))
with pytest.raises(SystemExit) as ex: with pytest.raises(SystemExit) as ex:
screens_mod.main() 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): 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 import knoe.ui.screens as screens_mod
cfg_path = tmp_path / "prole.cfg" 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)], ["install.py", "--silent", "--reset", "--delete-db", "-c", str(cfg_path)],
) )
monkeypatch.setattr(screens_mod, "_ensure_ansible_vault_credentials", lambda **_: None) monkeypatch.setattr(screens_mod, "_ensure_ansible_vault_credentials", lambda **_: None)
monkeypatch.setattr(screens_mod, "ProleController", MagicMock(return_value=MagicMock())) monkeypatch.setattr(screens_mod, "KnoeController", MagicMock(return_value=MagicMock()))
monkeypatch.setattr(screens_mod, "ProleConsoleInstaller", installer_factory) monkeypatch.setattr(screens_mod, "KnoeConsoleInstaller", installer_factory)
with pytest.raises(SystemExit) as ex: with pytest.raises(SystemExit) as ex:
screens_mod.main() 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): def test_repair_flag_does_not_trigger_silent_install(tmp_path):
"""--repair must dispatch to run_repair(), NOT run() (full install).""" """--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) installer = _make_installer(tmp_path)
@ -241,9 +241,9 @@ def test_repair_flag_does_not_trigger_silent_install(tmp_path):
run_repair_called = [] run_repair_called = []
with ( 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( patch.object(
ProleConsoleInstaller, KnoeConsoleInstaller,
"run_repair", "run_repair",
side_effect=lambda: run_repair_called.append(1) or 0, 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): def test_update_calls_repair_first(tmp_path):
"""run_update() must invoke run_repair() before applying config changes.""" """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) installer = _make_installer(tmp_path)
call_order: list[str] = [] call_order: list[str] = []
with ( with (
patch.object( patch.object(
ProleConsoleInstaller, KnoeConsoleInstaller,
"run_repair", "run_repair",
side_effect=lambda: call_order.append("repair") or 0, side_effect=lambda: call_order.append("repair") or 0,
), ),
patch.object(ProleConsoleInstaller, "_load_inputs_from_cfg", return_value={}), patch.object(KnoeConsoleInstaller, "_load_inputs_from_cfg", return_value={}),
patch.object(ProleConsoleInstaller, "_default_inputs", return_value={}), patch.object(KnoeConsoleInstaller, "_default_inputs", return_value={}),
patch.object(ProleConsoleInstaller, "_service_namespace", return_value="test-ns"), patch.object(KnoeConsoleInstaller, "_service_namespace", return_value="test-ns"),
patch.object(ProleConsoleInstaller, "_script_env_for_namespace", return_value={}), patch.object(KnoeConsoleInstaller, "_script_env_for_namespace", return_value={}),
patch.object( patch.object(
ProleConsoleInstaller, KnoeConsoleInstaller,
"_reconcile_supabase", "_reconcile_supabase",
side_effect=lambda env: call_order.append("reconcile"), side_effect=lambda env: call_order.append("reconcile"),
), ),
patch.object( patch.object(
ProleConsoleInstaller, KnoeConsoleInstaller,
"_step_db_build", "_step_db_build",
side_effect=lambda: call_order.append("db_build"), side_effect=lambda: call_order.append("db_build"),
), ),
patch.object( patch.object(
ProleConsoleInstaller, KnoeConsoleInstaller,
"_step_cnpg_deploy", "_step_cnpg_deploy",
side_effect=lambda: call_order.append("cnpg_deploy"), side_effect=lambda: call_order.append("cnpg_deploy"),
), ),
patch.object(ProleConsoleInstaller, "_write_cfg", return_value=None), patch.object(KnoeConsoleInstaller, "_write_cfg", return_value=None),
patch.object(ProleConsoleInstaller, "_close_log_file", return_value=None), patch.object(KnoeConsoleInstaller, "_close_log_file", return_value=None),
): ):
installer.run_update() 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): def test_update_forces_cnpg_rollout_only_for_deploy_step(tmp_path):
"""run_update() must force rollout for CNPG deploy, then restore configured value.""" """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) installer = _make_installer(tmp_path)
seen_force_rollout: list[bool] = [] seen_force_rollout: list[bool] = []
with ( with (
patch.object(ProleConsoleInstaller, "run_repair", return_value=0), patch.object(KnoeConsoleInstaller, "run_repair", return_value=0),
patch.object( patch.object(
ProleConsoleInstaller, KnoeConsoleInstaller,
"_load_inputs_from_cfg", "_load_inputs_from_cfg",
return_value={ return_value={
"init_cnpg_deploy.run_deploy": "true", "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", "init_cluster.supabase_enabled": "false",
}, },
), ),
patch.object(ProleConsoleInstaller, "_default_inputs", return_value={}), patch.object(KnoeConsoleInstaller, "_default_inputs", return_value={}),
patch.object(ProleConsoleInstaller, "_service_namespace", return_value="test-ns"), patch.object(KnoeConsoleInstaller, "_service_namespace", return_value="test-ns"),
patch.object(ProleConsoleInstaller, "_script_env_for_namespace", return_value={}), patch.object(KnoeConsoleInstaller, "_script_env_for_namespace", return_value={}),
patch.object(ProleConsoleInstaller, "_write_cfg", return_value=None), patch.object(KnoeConsoleInstaller, "_write_cfg", return_value=None),
patch.object(ProleConsoleInstaller, "_close_log_file", return_value=None), patch.object(KnoeConsoleInstaller, "_close_log_file", return_value=None),
patch.object( patch.object(
ProleConsoleInstaller, KnoeConsoleInstaller,
"_step_cnpg_deploy", "_step_cnpg_deploy",
side_effect=lambda: seen_force_rollout.append( side_effect=lambda: seen_force_rollout.append(
installer._get_input_bool("init_cnpg_deploy.force_rollout", False) 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): def test_reconcile_supabase_skips_studio_when_disabled(tmp_path):
"""_reconcile_supabase() must skip Studio when supabase_studio_enabled=false.""" """_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) installer = _make_installer(tmp_path)
base = _defaults(installer) base = _defaults(installer)
@ -502,7 +502,7 @@ def test_reconcile_supabase_skips_studio_when_disabled(tmp_path):
studio_reconcile_called = [] studio_reconcile_called = []
with patch.object( with patch.object(
ProleConsoleInstaller, KnoeConsoleInstaller,
"_reconcile_supabase_studio", "_reconcile_supabase_studio",
side_effect=lambda env: studio_reconcile_called.append(1), 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): def test_reconcile_supabase_calls_studio_when_enabled(tmp_path):
"""_reconcile_supabase() must invoke Studio reconcile when enabled.""" """_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) installer = _make_installer(tmp_path)
base = _defaults(installer) base = _defaults(installer)
@ -527,7 +527,7 @@ def test_reconcile_supabase_calls_studio_when_enabled(tmp_path):
studio_reconcile_called = [] studio_reconcile_called = []
with patch.object( with patch.object(
ProleConsoleInstaller, KnoeConsoleInstaller,
"_reconcile_supabase_studio", "_reconcile_supabase_studio",
side_effect=lambda env: studio_reconcile_called.append(1), side_effect=lambda env: studio_reconcile_called.append(1),
): ):

View File

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