diff --git a/conf/prod/prole.cfg b/conf/prod/prole.cfg index e1ee4cc..398d37b 100644 --- a/conf/prod/prole.cfg +++ b/conf/prod/prole.cfg @@ -1,5 +1,5 @@ ; Prole Master Configuration File -; Generated by install.py on 2026-03-27 21:57:14 +; Generated by install.py on 2026-03-31 00:08:32 ; This file is used as input for Ansible deployment and k8s cluster creation. [User] @@ -13,10 +13,12 @@ build.run_build = false database_options.distribution = percona database_options.ext.dblink = true database_options.ext.pg_buffercache = true +database_options.ext.pg_cron = true database_options.ext.pg_freespacemap = true database_options.ext.pg_repack = true database_options.ext.pg_stat_monitor = true database_options.ext.pg_stat_statements = true +database_options.ext.pg_tde = true database_options.ext.pgaudit = true database_options.ext.pgbadger = true database_options.ext.pgcrypto = true @@ -51,7 +53,7 @@ init_cluster.cluster_env = prod init_cluster.deployment_target = prole-prod-cluster init_cluster.gitops_enabled = false init_cluster.k3s_server_url = https://myrddin.prole.org:6443 -init_cluster.k3s_token = ${PROLE_SECRET:v1:WmcrTDHDhUyzeCC4:DvGQhp_kiVfcbpTCDncA_t-xH4ktjvCka7jst-7LjWLb-k0NicaNogLRwFT8b5_pJsPnBFnNhcrz-2iD4Zb0-L3i0gILy8LIwwJajRaic9qDNd01Wm_9kRjv_z2XRhFWiwGEm7m3sd__6BYkeKhBXE9lzii8WpGJgo9bAA==} +init_cluster.k3s_token = ${PROLE_SECRET:v1:h4uThQOAj6ASFIFf:_cS5t3-Z3q90-OWiMsX5BnLQnjC8tbomoyuv_UfnNM6XmI2H6yofzJWSyhgNfgFaWxL8ImIwYWVKf9OjvwEhxr54LVSQ9EEm3KQsRW1cADHWnkjl7AGifsi2Wa9ve6E0PueVPAyHC5cIUXJ3Njt_bdegibhjZv6_-jFkGg==} init_cluster.kerberos_enabled = true init_cluster.mode = k8s init_cluster.start_cluster = true @@ -62,14 +64,14 @@ init_db_build.run_build = true init_password.cluster_name = ${CLUSTER_NAME} init_password.db_host_port = 5432 init_password.db_namespace = ${DATABASE_NAMESPACE} -init_password.db_password = ${PROLE_SECRET:v1:eN8L5Bo1KE2xGU-Z:y4ybETpw1POYjX2tP96lWTajMLFsiTXn5DNj7s_l6gBZInnGB5MjuxQTZ-G_FBnK} -init_password.db_password_confirm = ${PROLE_SECRET:v1:eN8L5Bo1KE2xGU-Z:y4ybETpw1POYjX2tP96lWTajMLFsiTXn5DNj7s_l6gBZInnGB5MjuxQTZ-G_FBnK} +init_password.db_password = ${PROLE_SECRET:v1:uLCCn7fAxP1AHcv7:zahrhbz50ngH59bVmJUvbCm5OTM1Id_ksd5D61I-GixJETQVMewsr1oC7Avu8m6E} +init_password.db_password_confirm = ${PROLE_SECRET:v1:uLCCn7fAxP1AHcv7:zahrhbz50ngH59bVmJUvbCm5OTM1Id_ksd5D61I-GixJETQVMewsr1oC7Avu8m6E} init_password.db_username = root init_password.generate_ssh_key = true init_scripts.run_scripts = true kerberos_config.enabled = true kerberos_config.kdc = 10.0.0.3 -kerberos_config.password = ${PROLE_SECRET:v1:1UPXXGF4GW7MIfhR:_hFj8SAVHRd60qnOw-q5MaM9W8CaxNHgPkFnyXmNXXNzCj5x} +kerberos_config.password = ${PROLE_SECRET:v1:O-RQRUsyZ-C-oV7k:-9LlOE5KxpT8qGApgzzH2IneU76fYbCk-GwTsAII1tzxK_P3} kerberos_config.realm = PROLE.ORG kerberos_config.test_connection = false kerberos_config.user = administrator @@ -77,25 +79,28 @@ network_scan.run = true ollama_config.model = ollama_config.server_host = fairyland.prole.org ollama_config.server_port = 11434 +supabase_config.pv_base_dir = /synology/d005 +supabase_config.pv_node = gandalf.prole.org [Global] ; Variables used by name in more than one place or assumed global scope ARGOCD_NAMESPACE = argocd CLUSTER_ENV = prod CLUSTER_NAME = knoe-db -DATABASE_NAMESPACE = knoe-db-17-7-043-18-140-17-7-043-18-140 +DATABASE_NAMESPACE = knoe-db-17-7-043-18-140-17-7-043-18-140-17-7-043-18-140 DB_HOST_PORT = 5432 -DB_PASSWORD = ${PROLE_SECRET:v1:hk7ryF9uC0HMNmE0:ZFtwyX85sqA61qZDuSLEL1gWwuC20r6qCKnTihsGvJ7PODH5XcIRSQ5Ms3Il8lMt} +DB_PASSWORD = ${PROLE_SECRET:v1:7FWWyYsJ27QWE7Hf:E99YJFxLGoz_QyHPH7odXmCk6WBuYL3tWU0iBdpcHmb1LBph_VBAa5GyHmH6LVYP} DEPLOYMENT_MODE = k8s DEPLOYMENT_TARGET = prole-prod-cluster -DOCKER_IMPORT_DIR = DOCKER_PRELOAD = false KNOE_DB_USER = root OPTIONAL_WORKLOADS_MIN_READY_SCHEDULABLE_NODES = 2 PROLE_K3S_SERVER = https://myrddin.prole.org:6443 -PROLE_K3S_TOKEN = ${PROLE_SECRET:v1:eRF7V3BH7FObAF2t:omapQMsXQQA765nO3w8Y7LY5Hn4C1Mrglwk9xyNHEpQoKPBgrbd8aGZwtPE3UgmT_i5eeW110q0aWJtR4kwuK5q6LR9Ec2iHknIJYXA-v6isvhsPK8az1A8rE9kIVcahABT9mSEFmdNPk-UNvISsxqtOZ98fCXoFbfWegg==} -PROLE_OPENTOFU_URL = http://127.0.0.1:8080 +PROLE_K3S_TOKEN = ${PROLE_SECRET:v1:dcAmcZGfYyHgaY_0:wtuc48pEhK9Ha7BC3XG57MCf_4Wqgs09g3Jam2VQIc_3jNMIEIxORsDxGJsqZtyN0zmdSxs1ApZos-h6h3R5AkEZsmqdw6TTcFpixP0RwuPWCe5ObWIMyPR3ztWcwDiM69fm1QiFUNGPvARdyw6dBgyRKRqDpVSfXMpM5w==} SERVICE_NAMESPACE = knoe-system +SUPABASE_PV_BASE = /synology/d005 +SUPABASE_PV_BASE_DIR = /synology/d005 +SUPABASE_PV_NODE = gandalf.prole.org [Welcome] ; No configuration values captured yet for this section. @@ -110,7 +115,7 @@ ANSIBLE_DOMAIN = prole.org ANSIBLE_INFRASTRUCTURE = /Users/chrisfu/dev/prole/infrastructure ANSIBLE_INVENTORY = /Users/chrisfu/dev/prole/infrastructure/inventory ANSIBLE_REALM = PROLE.ORG -ANSIBLE_TOPOLOGY = {"domain":"prole.org","realm":"PROLE.ORG","internal_records":{"aventage.prole.org":"10.0.0.206","fairyland.prole.org":"10.0.0.208","loghost.prole.org":"10.0.0.3","merlin.prole.org":"10.0.0.6","morana.prole.org":"10.0.0.66","morgoth.prole.org":"10.0.0.204","myrddin.prole.org":"10.0.0.3","pi.prole.org":"10.0.0.5","raspberry.prole.org":"10.0.0.4","retropie.prole.org":"10.0.0.207","synology.prole.org":"10.0.0.203","zinfandel.prole.org":"10.0.0.205"},"ad_dc":{"host":"myrddin.prole.org","ip":"10.0.0.3"},"k3s":{"server_url":"https://myrddin.prole.org:6443","server_host":"myrddin.prole.org","token_present":true},"groups":{"iscsi":["pi.prole.org","raspberry.prole.org","myrddin.prole.org","retropie.prole.org","merlin.prole.org"],"pihole":["pi.prole.org","raspberry.prole.org"],"ad_dc":["myrddin.prole.org"],"k3s_servers":["myrddin.prole.org"],"k3s_agents":["pi.prole.org","merlin.prole.org"],"k3s_hosts:children":["k3s_servers","k3s_agents"],"linux_hosts":["pi.prole.org","raspberry.prole.org","myrddin.prole.org","retropie.prole.org","merlin.prole.org"],"ssl_hosts":["myrddin.prole.org"],"mariadb_primary":["merlin.prole.org"],"mariadb_replica":["raspberry.prole.org"],"mariadb:children":["mariadb_primary","mariadb_replica"],"merlin_bootstrap":["merlin"]},"hosts":{"merlin":"10.0.0.6","merlin.prole.org":"10.0.0.6","myrddin.prole.org":"10.0.0.3","pi.prole.org":"10.0.0.5","raspberry.prole.org":"10.0.0.4","retropie.prole.org":"10.0.0.207"},"unmapped_hosts":["k3s_agents","k3s_servers","mariadb_primary","mariadb_replica"]} +ANSIBLE_TOPOLOGY = {"domain":"prole.org","realm":"PROLE.ORG","internal_records":{"aventage.prole.org":"10.0.0.206","fairyland.prole.org":"10.0.0.208","loghost.prole.org":"10.0.0.3","merlin.prole.org":"10.0.0.6","morana.prole.org":"10.0.0.66","morgoth.prole.org":"10.0.0.204","myrddin.prole.org":"10.0.0.3","pi.prole.org":"10.0.0.5","raspberry.prole.org":"10.0.0.4","retropie.prole.org":"10.0.0.207","synology.prole.org":"10.0.0.203","zinfandel.prole.org":"10.0.0.205"},"ad_dc":{"host":"myrddin.prole.org","ip":"10.0.0.3"},"k3s":{"server_url":"https://myrddin.prole.org:6443","server_host":"myrddin.prole.org","token_present":true},"groups":{"iscsi":["pi.prole.org","raspberry.prole.org","myrddin.prole.org","retropie.prole.org","merlin.prole.org","gandalf.prole.org"],"pihole":["pi.prole.org","raspberry.prole.org"],"ad_dc":["myrddin.prole.org"],"k3s_servers":["myrddin.prole.org"],"k3s_agents":["pi.prole.org","merlin.prole.org","gandalf.prole.org"],"k3s_hosts:children":["k3s_servers","k3s_agents"],"linux_hosts":["pi.prole.org","raspberry.prole.org","myrddin.prole.org","retropie.prole.org","merlin.prole.org","gandalf.prole.org"],"ssl_hosts":["myrddin.prole.org"],"mariadb_primary":["merlin.prole.org"],"mariadb_replica":["raspberry.prole.org"],"mariadb:children":["mariadb_primary","mariadb_replica"],"merlin_bootstrap":["merlin"],"k3s_hosts":["gandalf.prole.org"]},"hosts":{"merlin":"10.0.0.6","merlin.prole.org":"10.0.0.6","myrddin.prole.org":"10.0.0.3","pi.prole.org":"10.0.0.5","raspberry.prole.org":"10.0.0.4","retropie.prole.org":"10.0.0.207"},"unmapped_hosts":["gandalf.prole.org","k3s_agents","k3s_servers","mariadb_primary","mariadb_replica"]} KDC_ANSIBLE_DETECTED = 10.0.0.3 KDC_AUTO_DETECTED = 10.0.0.3 KERBEROS_AUTO_ENABLED = True @@ -133,7 +138,6 @@ PROLE_SERVICE = /Users/chrisfu/dev/prole/etc [Ollama] OLLAMA_HOST = http://fairyland.prole.org:11434 -OLLAMA_SERVERS = fairyland.prole.org:11434,k3d.localhost:11434,morgoth.prole.org:11434 OLLAMA_SERVER_HOST = fairyland.prole.org OLLAMA_SERVER_PORT = 11434 @@ -152,23 +156,20 @@ GITOPS_ENABLED = False [Dev Cluster (k3d)] CLUSTER_ENV = dev DISPLAY_NAME = knoe-dev-cluster -KUBECTL_CONTEXT = MODE = k3d [Service Cluster (k3s)] CLUSTER_ENV = prole-service-cluster DISPLAY_NAME = prole-service-cluster K3S_SERVER_URL = https://myrddin.prole.org:6443 -K3S_TOKEN = ${PROLE_SECRET:v1:U7foZs_vBN4-2fLv:wFtVuuFMcJ-R9zXZwdOqxfPsTmRFZSkyvKc_Kqo_kL1-ie8MxWU2M25t78O7EjW9dFxEAYRsPanjxYKDHDd8arRW17kGIqtzlcRM9sXQJimTpa-mMgNCFew28b99c-LuCZbeG3nZU72joGZN6TZ5ZjZZbvJLAOA1Hq6b0A==} +K3S_TOKEN = ${PROLE_SECRET:v1:ZBrRQReMetk0k4X5:yl6S40jqATR45Ivg5DxoGlo_32X7Q6Zz0BpKIzWO0iKpLe5xK4H-lYTxzucn6AsivxNrZTRjLsTiQ1CrtIcmtDJzOwPjobkt5t2XGKmcBrPoeoUjqSFJolp2-0ClawERDXTnqLHOQJhNLFpqxrRc_AiU0iHiT6DuuZZ9Kg==} MODE = k3s -PIPELINE_URL = http://127.0.0.1:8080 [Prod Cluster (k8s)] ARTIFACTS_DIR = /Users/chrisfu/dev/prole/data/staging CLUSTER_ENV = prole-prod-cluster DISPLAY_NAME = prole-prod-cluster MODE = k8s -PIPELINE_URL = http://127.0.0.1:8080 [Docker Build] ; No configuration values captured yet for this section. diff --git a/conf/service/prole.cfg b/conf/service/prole.cfg index ecc0c89..d623d16 100644 --- a/conf/service/prole.cfg +++ b/conf/service/prole.cfg @@ -1,5 +1,5 @@ ; Prole Master Configuration File -; Generated by install.py on 2026-03-29 22:54:52 +; Generated by install.py on 2026-03-31 00:16:16 ; This file is used as input for Ansible deployment and k8s cluster creation. [User] @@ -53,7 +53,7 @@ init_cluster.cluster_env = service init_cluster.deployment_target = prole-service-cluster init_cluster.gitops_enabled = false init_cluster.k3s_server_url = https://myrddin.prole.org:6443 -init_cluster.k3s_token = ${PROLE_SECRET:v1:ncw40j3021elkcqU:OO4QXoXGTi4ptxeNvqFV9WXNsh_f2QEqSYnJxIR-MW-0J50oItM9NLQeQT3rY2Hju2C2VlSyeuKi3-MBGsm3wzQl5m1yjuVQ9MKrT20u2_pjyom3GuIhfCEx6agfadRHH_9lAio3wTVdvZqBgrTRyMQ67Um07eZRsXBTUA==} +init_cluster.k3s_token = ${PROLE_SECRET:v1:AL731QFqjjFGqfFT:Xt95lYCIk-6iCHerFSJQpKwWGJMdr04v-4DawlGbM3OBLJ7qbHOSZ_oJm1WtHkvqpoM7BSJNwlEqHMV7R3gl7IFqVUtqJ_AoaRcLSPKE2Z6JGWkIatR6Vzw3J1cm1iZiM2M9wikI5Wqp3t4cdWTOTf7PUaQHkYbBG1PV3w==} init_cluster.kerberos_enabled = true init_cluster.mode = k3s init_cluster.start_cluster = true @@ -64,14 +64,14 @@ init_db_build.run_build = true init_password.cluster_name = ${CLUSTER_NAME} init_password.db_host_port = 5432 init_password.db_namespace = ${DATABASE_NAMESPACE} -init_password.db_password = ${PROLE_SECRET:v1:4QVLv6L_Zb6w1Zja:4PHGYWVndekQb3ZG7hjNAoIQ_T2yEZlS} -init_password.db_password_confirm = ${PROLE_SECRET:v1:4QVLv6L_Zb6w1Zja:4PHGYWVndekQb3ZG7hjNAoIQ_T2yEZlS} +init_password.db_password = ${PROLE_SECRET:v1:OcWBTOiijqf-bfTT:6r-ioWdDRdkPNxmbEAQP2TYEPaycWasP} +init_password.db_password_confirm = ${PROLE_SECRET:v1:OcWBTOiijqf-bfTT:6r-ioWdDRdkPNxmbEAQP2TYEPaycWasP} init_password.db_username = root init_password.generate_ssh_key = true init_scripts.run_scripts = true kerberos_config.enabled = true kerberos_config.kdc = 10.0.0.3 -kerberos_config.password = ${PROLE_SECRET:v1:lDmjQ4Cj45LYBD9_:9Aes8WNTZ97oYpi1aFvS8ODHDCQJJJCUBFgNrJBZ3a-Mgwp2} +kerberos_config.password = ${PROLE_SECRET:v1:h9mzEWvGpnpU2FRz:5W8czWfjnRxzA8tJPk3eFRtrVLD5DxREkfqmQsnKLsbuAaec} kerberos_config.realm = PROLE.ORG kerberos_config.test_connection = false kerberos_config.user = administrator @@ -79,6 +79,8 @@ network_scan.run = true ollama_config.model = ollama_config.server_host = fairyland.prole.org ollama_config.server_port = 11434 +supabase_config.pv_base_dir = /synology/d005 +supabase_config.pv_node = gandalf.prole.org [Global] ; Variables used by name in more than one place or assumed global scope @@ -92,7 +94,7 @@ CNPG_PLACEMENT_PLAN_ID = cnpg-placement-7036c5f65b6ef0db CNPG_STAGE1_NODE = gandalf.prole.org DATABASE_NAMESPACE = knoe-db DB_HOST_PORT = 5432 -DB_PASSWORD = ${PROLE_SECRET:v1:FgbkzVICzbA4WX7l:t5eAnQUYrTfAiXuUUPgZ8rBJVqAp-WFA} +DB_PASSWORD = ${PROLE_SECRET:v1:M7Rt2Xp273X8ixb5:Rjys8A6SqmBv19DvPAvgihwJ0eJ8Xvda} DEPLOYMENT_MODE = k3s DEPLOYMENT_TARGET = prole-service-cluster DOCKER_PRELOAD = false @@ -100,8 +102,11 @@ KNOE_DB_USER = root KUBECONTEXT = knoe.dev.prole.org OPTIONAL_WORKLOADS_MIN_READY_SCHEDULABLE_NODES = 2 PROLE_K3S_SERVER = https://myrddin.prole.org:6443 -PROLE_K3S_TOKEN = ${PROLE_SECRET:v1:ncw40j3021elkcqU:OO4QXoXGTi4ptxeNvqFV9WXNsh_f2QEqSYnJxIR-MW-0J50oItM9NLQeQT3rY2Hju2C2VlSyeuKi3-MBGsm3wzQl5m1yjuVQ9MKrT20u2_pjyom3GuIhfCEx6agfadRHH_9lAio3wTVdvZqBgrTRyMQ67Um07eZRsXBTUA==} +PROLE_K3S_TOKEN = ${PROLE_SECRET:v1:DkOrjdIC-ji9iusP:7uc4RUXpALlyQcGfZfX6LBP7BEwOqBtbbZpbIgdbfJvMQ_1klRdLtDQrcG54p6ayHvaMQ6W7FSPHPLeKbcjDZ8eJMhNMaH9Q0kIyfmiZdbX3nN6QGCxJWjIn-4GJ4z6_gF7hjHrqAAqxReNoWkAokyoVgqKTiOoWToD0GA==} SERVICE_NAMESPACE = knoe-system +SUPABASE_PV_BASE = /synology/d005 +SUPABASE_PV_BASE_DIR = /synology/d005 +SUPABASE_PV_NODE = gandalf.prole.org SYNOLOGY_ROOTS = /synology/d001,/synology/d002,/synology/d003,/synology/d004,/synology/d005 [Welcome] @@ -133,7 +138,7 @@ PROLE_LOGS = /Users/chrisfu/dev/prole/logs PROLE_SERVICE = /Users/chrisfu/dev/prole/etc [Monitoring] -; No configuration values captured yet for this section. +GRAFANA_ADMIN_PASSWORD = ${PROLE_SECRET:v1:st2uaIMwx55ftSPt:HjzyDuKpzGJyIDVzIKWD6YDzyoqZ4iJ9} [Kerberos Authentication] ; No configuration values captured yet for this section. @@ -158,7 +163,7 @@ SUPABASE_ENABLED = False [Initialize Cluster] ENVIRONMENT = service K3S_SERVER_URL = https://myrddin.prole.org:6443 -K3S_TOKEN = ${PROLE_SECRET:v1:ncw40j3021elkcqU:OO4QXoXGTi4ptxeNvqFV9WXNsh_f2QEqSYnJxIR-MW-0J50oItM9NLQeQT3rY2Hju2C2VlSyeuKi3-MBGsm3wzQl5m1yjuVQ9MKrT20u2_pjyom3GuIhfCEx6agfadRHH_9lAio3wTVdvZqBgrTRyMQ67Um07eZRsXBTUA==} +K3S_TOKEN = ${PROLE_SECRET:v1:sQeWAX7UamXXV4oR:O7sUJk54DE9gUtZAjmN4uiusuVl0T3SDTn-S4Ojgg844EOQV7dMhE1wncgn8kLXXz_klqkQszDzbTqQm2HdaQuFyCqnHGCtHyUAyFvXSAdhhQ3PSkggylLtbPQPPrEP6WFuFywdKf1QgLl2BOcipREupBj5XDfiNfEAigw==} [Dev Cluster (k3d)] CLUSTER_ENV = dev @@ -170,7 +175,7 @@ MODE = k3d CLUSTER_ENV = prole-service-cluster DISPLAY_NAME = prole-service-cluster K3S_SERVER_URL = https://myrddin.prole.org:6443 -K3S_TOKEN = ${PROLE_SECRET:v1:ncw40j3021elkcqU:OO4QXoXGTi4ptxeNvqFV9WXNsh_f2QEqSYnJxIR-MW-0J50oItM9NLQeQT3rY2Hju2C2VlSyeuKi3-MBGsm3wzQl5m1yjuVQ9MKrT20u2_pjyom3GuIhfCEx6agfadRHH_9lAio3wTVdvZqBgrTRyMQ67Um07eZRsXBTUA==} +K3S_TOKEN = ${PROLE_SECRET:v1:sast5Yrr_9mB5BzC:J8Et0XHuUbZbFzAovq-lgqdu_3rR4k3FHOJ7LmzCls-qcZV9D9_NNdmtw5gOTRA8kyZeP5oAFGAXKPGWHgxMJxGO_Lh2oQ1m_68_ExKCK7oeXxlZC3bFCd_fvQ-lP0thBYA9R-GkV_67tJfluOo632xhv6i3Pl1sgBZt5w==} MODE = k3s [Prod Cluster (k8s)] @@ -183,7 +188,7 @@ MODE = k8s ; No configuration values captured yet for this section. [Initialization Scripts] -STATUS = Attempted +STATUS = Completed [Deployment] MODE = k3s diff --git a/install.py b/install.py index 0a871d9..f405969 100755 --- a/install.py +++ b/install.py @@ -1,8 +1,6 @@ #!/usr/bin/env python3 """ Thin launch vehicle for the Prole installer. - -Keeps legacy UI available while exposing a stable import surface for tests. """ from __future__ import annotations @@ -13,16 +11,16 @@ from tkinter import ttk, scrolledtext, messagebox, filedialog # noqa: F401 from pathlib import Path # noqa: F401 from knoe.ui.screens import ( - ProleInstaller, - ProleController, + KnoeInstaller, + KnoeController, get_resource_path, has_display, main, ) __all__ = [ - "ProleInstaller", - "ProleController", + "KnoeInstaller", + "KnoeController", "get_resource_path", "has_display", "main", diff --git a/knoe-db.iml b/knoe-db.iml index 523eece..9bd88ac 100644 --- a/knoe-db.iml +++ b/knoe-db.iml @@ -2,10 +2,74 @@ - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/knoe/core/actions.py b/knoe/core/actions.py index 797b51b..e975938 100644 --- a/knoe/core/actions.py +++ b/knoe/core/actions.py @@ -4,17 +4,16 @@ Installer actions and unattended workflow helpers. from __future__ import annotations -import json -import logging -import secrets import shlex import socket -import string import sys import threading import uuid -from typing import Callable, Sequence +import logging +import secrets +import string +from knoe import prole_conf as prole_conf_mgr from knoe.config import ( _expand_path, _collect_cfg_vars, @@ -28,8 +27,13 @@ from knoe.config import ( _encrypt_cfg_secret, _merge_kubeconfig, ) -from knoe import prole_conf as prole_conf_mgr -from knoe.core.controller import ProleController +from knoe.core.build_context import copy_build_context_dir +from knoe.core.cnpg_placement import ( + load_cnpg_placement_plan, + plan_cnpg_placement, + save_cnpg_placement_plan, +) +from knoe.core.controller import KnoeController from knoe.core.env import * # noqa: F401,F403 from knoe.core.env import ( _parse_ollama_host, @@ -57,7 +61,6 @@ from knoe.core.env import ( _render_prole_cfg, _k3d_prole_data_volume_args, _sync_opentofu_pipeline, - _normalize_k3s_token, _read_k3s_cfg, _safe_str, _expand_cfg_value as _expand_env_cfg_value, @@ -80,34 +83,29 @@ from knoe.core.milestones import ( SupabaseImagePreloadMilestone, SupabaseMilestone, ) -from knoe.core.stream_exec import run_streaming_cmd -from knoe.core.build_context import copy_build_context_dir -from knoe.core.cnpg_placement import ( - load_cnpg_placement_plan, - plan_cnpg_placement, - save_cnpg_placement_plan, -) -from knoe.core.policy import ( - POLICY_CFG_KEY, - OPTIONAL_WORKLOADS_MIN_READY_SCHEDULABLE_NODES, - evaluate_optional_workloads_allowed, -) -from knoe.core.ops.storage import ( - ClusterStorageSpec, - StorageProvisioningError, - provision_cluster_storage, -) +from knoe.core.ops import garage_store as garage_store_ops +from knoe.core.ops import monitoring as monitoring_ops +from knoe.core.ops import openbao as openbao_ops +from knoe.core.ops import opentofu as opentofu_ops +from knoe.core.ops import registry as registry_ops from knoe.core.ops.cloudnative_pg import ( initialize as cnpg_initialize, deploy as cnpg_deploy, install_barman_plugin as cnpg_install_barman_plugin, rollout as cnpg_rollout, ) -from knoe.core.ops import openbao as openbao_ops -from knoe.core.ops import registry as registry_ops -from knoe.core.ops import garage_store as garage_store_ops -from knoe.core.ops import opentofu as opentofu_ops -from knoe.core.ops import monitoring as monitoring_ops +from knoe.core.ops.storage import ( + ClusterStorageSpec, + StorageProvisioningError, + provision_cluster_storage, +) +from knoe.core.policy import ( + POLICY_CFG_KEY, + OPTIONAL_WORKLOADS_MIN_READY_SCHEDULABLE_NODES, + evaluate_optional_workloads_allowed, +) +from knoe.core.stream_exec import run_streaming_cmd +from typing import Callable, Sequence def _configure_unbuffered_io(): @@ -157,7 +155,7 @@ def _select_existing_kubeconfig(candidates: Sequence[Path]) -> Path | None: return first_existing -class ProleInstaller: +class KnoeInstaller: """Shared business logic for both silent and interactive installer modes. Subclasses must provide a :meth:`_get_input` implementation that bridges @@ -3095,16 +3093,17 @@ class _TeeWriter: return getattr(self._original, name) -class ProleConsoleInstaller(ProleInstaller): +class KnoeConsoleInstaller(KnoeInstaller): """Console-based unattended installer driven by prole.cfg inputs.""" silent = True def __init__( self, - controller: ProleController, + controller: KnoeController, cfg_path: str | None = None, reset_cluster: bool = False, + delete_db: bool = False, ): self.controller = controller self.project_root = controller.project_root @@ -3114,6 +3113,7 @@ class ProleConsoleInstaller(ProleInstaller): self.controller.cfg_path = self.cfg_path self.reset_cluster = bool(reset_cluster) self.reset_requested = bool(reset_cluster) + self.delete_db_requested = bool(delete_db) # Keep a persistent indicator for reset-oriented reconcile actions (e.g. # reclaiming safe stale Released PV claimRefs). `_perform_cluster_reset()` # consumes `reset_requested` and clears it, but we still want to run @@ -6025,9 +6025,15 @@ class ProleConsoleInstaller(ProleInstaller): # Ensure we have a cert-based kubeconfig (or a valid bearer token config) # before invoking any kubectl-based reset scripts. self._ensure_project_k3s_kubeconfig(server, token) - self.log( - f"[RESET] Namespaces to clean (PVCs preserved): {', '.join(reset_namespaces)}" - ) + if self.delete_db_requested: + self.log(f"[RESET] Namespaces to clean: {', '.join(reset_namespaces)}") + self.log( + f"[RESET] --delete-db enabled; database PVCs in namespace '{db_ns}' will be deleted." + ) + else: + self.log( + f"[RESET] Namespaces to clean (PVCs preserved): {', '.join(reset_namespaces)}" + ) self.log( f"[RESET] Preserving registry workload in namespace: {registry_ns}" ) @@ -6038,10 +6044,13 @@ class ProleConsoleInstaller(ProleInstaller): token, registry_namespace=registry_ns, clear_node_reservations=True, + delete_pvcs_namespaces=[db_ns] if self.delete_db_requested else None, + force_delete_bound_pvs=self.delete_db_requested, ) self._cleanup_local_k3s_artifacts() self.reset_requested = False + self.delete_db_requested = False def _cleanup_local_k3s_artifacts(self) -> None: paths = [ @@ -6571,7 +6580,7 @@ def _run_silent_install_test(project_root: Path, cfg_path: Path) -> int: def _attempt_k3s_repair( - controller: ProleController, + controller: KnoeController, namespace: str, server: str, token: str, @@ -6617,6 +6626,8 @@ def _reset_k3s_namespace( *, registry_namespace: str | None = None, clear_node_reservations: bool = False, + delete_pvcs_namespaces: Sequence[str] | None = None, + force_delete_bound_pvs: bool = False, ) -> None: script = project_root / "scripts" / "reset-ns.sh" if not script.exists(): @@ -6649,10 +6660,82 @@ def _reset_k3s_namespace( env["KUBECONFIG"] = str(kubeconfig_path) reg_ns = (registry_namespace or "").strip() + delete_pvcs_set = { + str(ns or "").strip() + for ns in (delete_pvcs_namespaces or []) + if str(ns or "").strip() + } + + def _collect_bound_pvs(target_ns: str) -> list[str]: + res = subprocess.run( + [ + "kubectl", + "-n", + target_ns, + "get", + "pvc", + "-o", + "jsonpath={range .items[*]}{.spec.volumeName}{\"\\n\"}{end}", + ], + env=env, + capture_output=True, + text=True, + ) + if res.returncode != 0: + print( + f"[WARN] Could not list PVC-bound PVs in {target_ns} (code {res.returncode}).", + file=sys.stderr, + ) + return [] + pvs: list[str] = [] + for line in (res.stdout or "").splitlines(): + pv = (line or "").strip() + if pv and pv not in pvs: + pvs.append(pv) + return pvs + + def _set_reclaim_policy_delete(pvs: Sequence[str]) -> None: + for pv in pvs: + patch = '{"spec":{"persistentVolumeReclaimPolicy":"Delete"}}' + res = subprocess.run( + ["kubectl", "patch", "pv", pv, "--type=merge", "-p", patch], + env=env, + capture_output=True, + text=True, + ) + if res.returncode != 0: + print( + f"[WARN] Failed to set reclaimPolicy=Delete for pv/{pv} (code {res.returncode}).", + file=sys.stderr, + ) + + def _delete_pvs(pvs: Sequence[str]) -> None: + for pv in pvs: + res = subprocess.run( + ["kubectl", "delete", "pv", pv, "--ignore-not-found", "--wait=false"], + env=env, + capture_output=True, + text=True, + ) + if res.returncode != 0: + print( + f"[WARN] Failed to delete pv/{pv} (code {res.returncode}).", + file=sys.stderr, + ) + for idx, ns in enumerate(namespaces): + delete_pvcs = ns in delete_pvcs_set + bound_pvs: list[str] = [] + if delete_pvcs and force_delete_bound_pvs: + bound_pvs = _collect_bound_pvs(ns) + if bound_pvs: + _set_reclaim_policy_delete(bound_pvs) + cmd = ["bash", str(script), "-n", ns] if reg_ns and ns == reg_ns: cmd.append("--keep-registry") + if delete_pvcs: + cmd.append("--delete-pvcs") if clear_node_reservations and idx == (len(namespaces) - 1): cmd.append("--clear-node-reservations") res = subprocess.run(cmd, env=env) @@ -6661,10 +6744,13 @@ def _reset_k3s_namespace( f"[WARN] Namespace reset failed for {ns} (code {res.returncode})", file=sys.stderr, ) + continue + if delete_pvcs and force_delete_bound_pvs and bound_pvs: + _delete_pvs(bound_pvs) def _prepare_k3s_pipeline( - controller: ProleController, log_fn=None, skip_validation: bool = False + controller: KnoeController, log_fn=None, skip_validation: bool = False ) -> int: """Prepare OpenTofu k3s pipeline: stage manifests, tfvars, ArgoCD apps. @@ -6702,7 +6788,7 @@ def _prepare_k3s_pipeline( # Load config to resolve namespace and secrets for pipeline tfvars _log("==> Loading knoe config...\n") - knoe = ProleConsoleInstaller(controller, str(cfg_path)) + knoe = KnoeConsoleInstaller(controller, str(cfg_path)) try: existing_inputs = knoe._load_inputs_from_cfg() except Exception: diff --git a/knoe/core/controller.py b/knoe/core/controller.py index efe14a2..c9c9fd1 100644 --- a/knoe/core/controller.py +++ b/knoe/core/controller.py @@ -14,14 +14,14 @@ if TYPE_CHECKING: from knoe.milestone import Milestone -class ProleController: +class KnoeController: """Business logic for Prole installer, separated from UI.""" def __init__(self, project_root: Path, verbose: bool = False, cfg_path: Path | None = None): self.project_root = project_root self.verbose = verbose self.cfg_path = cfg_path - self.logger = logging.getLogger("ProleController") + self.logger = logging.getLogger("KnoeController") self.state = InstallerState(controller=self) def check_docker_running(self) -> bool: diff --git a/knoe/core/monitor.py b/knoe/core/monitor.py index 39a9d17..ca5ac0f 100644 --- a/knoe/core/monitor.py +++ b/knoe/core/monitor.py @@ -9,13 +9,10 @@ import sys import time from pathlib import Path -from knoe.config import _expand_path, _collect_cfg_vars, _expand_cfg_value from knoe import prole_conf +from knoe.config import _expand_path, _collect_cfg_vars, _expand_cfg_value from knoe.core.env import ( _build_required_port_forwards, - _pf_extract_id, - _pf_mapping_str, - _pf_upsert_mapping, ) diff --git a/knoe/core/ops/cloudnative_pg.py b/knoe/core/ops/cloudnative_pg.py index 9786595..9771b81 100644 --- a/knoe/core/ops/cloudnative_pg.py +++ b/knoe/core/ops/cloudnative_pg.py @@ -21,23 +21,23 @@ Public API rollout(namespace, cluster_name, env, log) -> None """ from __future__ import annotations + import base64 import datetime -import json -import logging import subprocess import time from pathlib import Path -from typing import Callable +import json +import logging from cryptography import x509 from cryptography.hazmat.primitives import hashes, serialization from cryptography.hazmat.primitives.asymmetric.ec import ( - ECDSA, SECP256R1, generate_private_key, ) from cryptography.x509.oid import NameOID +from typing import Callable _LOG = logging.getLogger(__name__) _LogFn = Callable[[str], None] diff --git a/knoe/core/repair_pipeline_cli.py b/knoe/core/repair_pipeline_cli.py index 662558d..739463b 100644 --- a/knoe/core/repair_pipeline_cli.py +++ b/knoe/core/repair_pipeline_cli.py @@ -6,8 +6,8 @@ import sys from pathlib import Path from knoe import prole_conf -from knoe.core.controller import ProleController -from knoe.core.actions import ProleConsoleInstaller +from knoe.core.controller import KnoeController +from knoe.core.actions import KnoeConsoleInstaller def _project_root() -> Path: @@ -80,8 +80,8 @@ def main(argv: list[str] | None = None) -> int: if not cfg: cfg = str(prole_conf.entrypoint_path(prole_conf.resolve_prole_conf_dir(root))) - controller = ProleController(project_root=root, verbose=bool(args.verbose), cfg_path=Path(cfg)) - installer = ProleConsoleInstaller(controller, cfg_path=cfg) + controller = KnoeController(project_root=root, verbose=bool(args.verbose), cfg_path=Path(cfg)) + installer = KnoeConsoleInstaller(controller, cfg_path=cfg) try: existing_inputs = knoe._load_inputs_from_cfg() diff --git a/knoe/ncurses_installer.py b/knoe/ncurses_installer.py index 2575bc9..41736e0 100644 --- a/knoe/ncurses_installer.py +++ b/knoe/ncurses_installer.py @@ -18,22 +18,21 @@ import time from pathlib import Path from . import config as inst_config -from .ncurses_ui import CursesWindow, NavFooter, InputField -from .core.stream_exec import run_streaming_cmd +from .config import _write_k3s_kubeconfig, _merge_kubeconfig from .core.env import ( _resolve_k3s_connection, _verify_k3s_services_status, _find_kubeconfig_file, - _read_k3s_cfg, resolve_prole_home, ) from .core.ops import openbao as openbao_ops -from .core.ops import registry as registry_ops from .core.ops import opentofu as opentofu_ops -from .config import _write_k3s_kubeconfig, _merge_kubeconfig +from .core.ops import registry as registry_ops +from .core.stream_exec import run_streaming_cmd +from .ncurses_ui import CursesWindow, NavFooter, InputField -class ProleNcursesInstaller: +class KnoeNcursesInstaller: """Ncurses-based installer matching install.py screen flow.""" def __init__(self, stdscr, controller): @@ -974,7 +973,7 @@ def run_ncurses_installer(controller): """Entry point for ncurses installer.""" def _main(stdscr): - installer = ProleNcursesInstaller(stdscr, controller) + installer = KnoeNcursesInstaller(stdscr, controller) knoe.run() try: diff --git a/knoe/ui/screens/__init__.py b/knoe/ui/screens/__init__.py index f67d585..824b458 100644 --- a/knoe/ui/screens/__init__.py +++ b/knoe/ui/screens/__init__.py @@ -1,7 +1,7 @@ """ Prole Service Dependencies Installer — screen package. -This package splits the monolithic ``ProleInstaller`` class into cohesive +This package splits the monolithic ``KnoeInstaller`` class into cohesive mixin modules, one per logical screen / concern. The composed class is assembled here and re-exported so that external callers (``install.py``, tests) see the same public surface as before. @@ -10,7 +10,6 @@ tests) see the same public surface as before. from __future__ import annotations import configparser -import platform import signal import socket import threading @@ -18,45 +17,18 @@ import tkinter as tk import uuid from tkinter import ttk, messagebox, filedialog -from knoe import deploy as inst_deploy -from knoe import prole_conf +import platform from knoe import screen as ui -from knoe.build import get_build_command as inst_get_build_command from knoe.config import ( _expand_cfg_value, _collect_cfg_vars, - _is_prole_secret, - _decrypt_prole_secret, - _is_openbao_ref, - _write_k3s_kubeconfig, - _encrypt_cfg_secret, - _encrypt_prole_secret, ) -from knoe.core.controller import ProleController +from knoe.core.controller import KnoeController from knoe.core.env import ( - _detect_ansible_topology, - _format_ansible_topology_summary, - _normalize_k3s_token, _deployment_target_label, - _cluster_env_radio_value, - _find_kubeconfig_file, - _bool_str, - _looks_like_k8s_bearer_token, - _deployment_mode_from_env, - _pf_extract_id, _ensure_ansible_vault_credentials, - _openbao_placeholder, - _pf_upsert_mapping, - _default_opentofu_pipeline_url, - _build_required_port_forwards, - _render_prole_cfg, _safe_str, _normalize_cluster_env, - _k3d_prole_data_volume_args, - _push_docker_image, - _http_ping_registry, - _local_registry_enabled, - _collect_images_from_files, ) # On macOS, set process name early so the menu bar reads correctly. @@ -70,10 +42,9 @@ if platform.system() == "Darwin": from knoe.core.env import * # noqa: F401,F403 from knoe.core.actions import ( - ProleInstaller, - ProleConsoleInstaller, + KnoeInstaller, + KnoeConsoleInstaller, _prepare_k3s_pipeline, - _reset_k3s_namespace, ) # Keep global frames list in memory (legacy requirement). @@ -108,8 +79,8 @@ from knoe.ui.screens.cfg import ConfigMixin # --------------------------------------------------------------------------- # Composed installer class # --------------------------------------------------------------------------- -class ProleInstaller( - ProleInstaller, +class KnoeInstaller( + KnoeInstaller, ScreenBaseMixin, NavigationMixin, WelcomeScreenMixin, @@ -212,7 +183,7 @@ class ProleInstaller( conf_dir = prole_conf.resolve_prole_conf_dir(PROJECT_ROOT) resolved_cfg = prole_conf.entrypoint_path(conf_dir) - self.controller = ProleController( + self.controller = KnoeController( PROJECT_ROOT, verbose=verbose, cfg_path=resolved_cfg ) self.project_root = PROJECT_ROOT @@ -734,6 +705,21 @@ class ProleInstaller( self.kerberos_password = tk.StringVar() self.kerberos_kdc = tk.StringVar() self.supabase_enabled = tk.BooleanVar(value=False) + _global_cfg = self.prole_cfg_data.get("Global", {}) or {} + _supabase_pv_node = ( + (_global_cfg.get("SUPABASE_PV_NODE", "") or "").strip() + or (os.environ.get("SUPABASE_PV_NODE") or "").strip() + or "gandalf.prole.org" + ) + _supabase_pv_base = ( + (_global_cfg.get("SUPABASE_PV_BASE_DIR", "") or "").strip() + or (_global_cfg.get("SUPABASE_PV_BASE", "") or "").strip() + or (os.environ.get("SUPABASE_PV_BASE_DIR") or "").strip() + or (os.environ.get("SUPABASE_PV_BASE") or "").strip() + or "/synology/d005" + ) + self.supabase_pv_node = tk.StringVar(value=_supabase_pv_node) + self.supabase_pv_base_dir = tk.StringVar(value=_supabase_pv_base) self.at_rest_encryption_enabled = tk.BooleanVar(value=True) self.ollama_server_host = tk.StringVar() self.ollama_server_port = tk.StringVar(value=DEFAULT_OLLAMA_PORT) @@ -891,6 +877,12 @@ def main(): action="store_true", help="Reset k3d dev cluster or k3s service cluster", ) + parser.add_argument( + "--delete-db", + action="store_true", + default=False, + help=argparse.SUPPRESS, + ) parser.add_argument( "--no-gui", action="store_true", @@ -919,6 +911,9 @@ def main(): parser.add_argument("command_args", nargs="*", help="Arguments for the subcommand") args = parser.parse_args() + if args.delete_db and not args.reset: + parser.error("--delete-db requires --reset") + # Set up log file tee if --log is requested log_file_path = None if args.log is not None: @@ -933,7 +928,7 @@ def main(): setup_logging(verbose=args.verbose, debug=args.debug) - controller = ProleController( + controller = KnoeController( PROJECT_ROOT, verbose=args.verbose, cfg_path=Path(args.config).expanduser() if args.config else None, @@ -953,8 +948,11 @@ def main(): if args.silent: _ensure_ansible_vault_credentials(prompt_ui=False) - installer = ProleConsoleInstaller( - controller, args.config, reset_cluster=args.reset + installer = KnoeConsoleInstaller( + controller, + args.config, + reset_cluster=args.reset, + delete_db=args.delete_db, ) if log_file_path: installer.enable_log_file(log_file_path) @@ -994,7 +992,7 @@ def main(): except Exception: root.geometry(f"{window_width}x{window_height}") _ensure_ansible_vault_credentials(prompt_ui=True, root=root) - ProleInstaller( + KnoeInstaller( root, config_path=args.config, reset_cluster=args.reset, diff --git a/knoe/ui/screens/base.py b/knoe/ui/screens/base.py index 57c822a..1e6e8a3 100644 --- a/knoe/ui/screens/base.py +++ b/knoe/ui/screens/base.py @@ -2,10 +2,10 @@ import os import subprocess -from datetime import datetime import tkinter as tk +from datetime import datetime from tkinter import ttk, messagebox, filedialog -from knoe.screen import TerminalConsole + from knoe import screen as ui diff --git a/knoe/ui/screens/build.py b/knoe/ui/screens/build.py index 7a9ca3d..ca9b973 100644 --- a/knoe/ui/screens/build.py +++ b/knoe/ui/screens/build.py @@ -1,20 +1,20 @@ """Build screen, terminal console overlay and process management.""" import os -import platform import re import shlex import signal import subprocess import threading import time -import webbrowser import tkinter as tk +import webbrowser from tkinter import ttk, messagebox, filedialog -from knoe.screen import TerminalConsole + +import platform from knoe import deploy as inst_deploy -from knoe.build import get_build_command as inst_get_build_command from knoe import screen as ui +from knoe.build import get_build_command as inst_get_build_command from knoe.core.env import PROJECT_ROOT diff --git a/knoe/ui/screens/cfg.py b/knoe/ui/screens/cfg.py index 95c0cc6..ac3da57 100644 --- a/knoe/ui/screens/cfg.py +++ b/knoe/ui/screens/cfg.py @@ -2,14 +2,17 @@ import configparser import os -import platform import socket -import string import subprocess -from pathlib import Path import tkinter as tk +from pathlib import Path from tkinter import ttk, messagebox, filedialog -from knoe import prole_conf + +import platform +from knoe.config import ( + _encrypt_cfg_secret, + _filter_cfg_values_for_persistence, +) from knoe.core.env import ( DEFAULT_ACTION_FLAGS, DEFAULT_OLLAMA_PORT, @@ -28,12 +31,6 @@ from knoe.core.env import ( _read_k3s_cfg, _render_prole_cfg, ) -from knoe.config import ( - _collect_cfg_vars, - _encrypt_cfg_secret, - _expand_cfg_value, - _filter_cfg_values_for_persistence, -) class ConfigMixin: @@ -112,6 +109,9 @@ class ConfigMixin: "DATABASE_NAMESPACE": (self.db_namespace.get() or "").strip(), "CLUSTER_NAME": (self.cnpg_cluster_name.get() or "").strip(), "DB_HOST_PORT": (self.db_host_port.get() or "5432").strip(), + "SUPABASE_PV_NODE": (self.supabase_pv_node.get() or "").strip(), + "SUPABASE_PV_BASE": (self.supabase_pv_base_dir.get() or "").strip(), + "SUPABASE_PV_BASE_DIR": (self.supabase_pv_base_dir.get() or "").strip(), "PROLE_OPENTOFU_URL": _default_opentofu_pipeline_url(), } if prole_home_override: @@ -477,6 +477,8 @@ class ConfigMixin: DEFAULT_ACTION_FLAGS.get("init_cluster.start_cluster", True), ), ) + _set("supabase_config.pv_node", _get_var(self.supabase_pv_node, "")) + _set("supabase_config.pv_base_dir", _get_var(self.supabase_pv_base_dir, "")) # Kerberos config _set_bool("kerberos_config.enabled", _get_var(self.kerberos_enabled, False)) diff --git a/knoe/ui/screens/cluster.py b/knoe/ui/screens/cluster.py index a4ffc22..0271caa 100644 --- a/knoe/ui/screens/cluster.py +++ b/knoe/ui/screens/cluster.py @@ -1,34 +1,18 @@ """Cluster lifecycle management (k3d / k3s / k8s).""" -import json import os import re import subprocess import sys import threading import time -import urllib.error -import urllib.request -from pathlib import Path import tkinter as tk +from pathlib import Path from tkinter import ttk, messagebox, filedialog -from knoe.screen import TerminalConsole -from knoe import screen as ui + +import json from knoe import prole_conf -from knoe.core.env import ( - PROJECT_ROOT, - _default_opentofu_pipeline_url, - _deployment_mode_from_env, - _deployment_target_label, - _find_kubeconfig_file, - _k3d_prole_data_volume_args, - _kubectl_base_cmd_for_k3s as _kubectl_base_cmd_for_k3s_fn, - _looks_like_k8s_bearer_token, - _normalize_cluster_env, - _normalize_k3s_token, - _resolve_k3s_connection as _resolve_k3s_connection_fn, - _safe_str, -) +from knoe import screen as ui from knoe.config import ( _collect_cfg_vars_from_data, _encrypt_cfg_secret, @@ -37,6 +21,18 @@ from knoe.config import ( _write_k3s_kubeconfig, ) from knoe.core.actions import _reset_k3s_namespace +from knoe.core.env import ( + PROJECT_ROOT, + _default_opentofu_pipeline_url, + _deployment_mode_from_env, + _deployment_target_label, + _find_kubeconfig_file, + _k3d_prole_data_volume_args, + _normalize_cluster_env, + _normalize_k3s_token, + _resolve_k3s_connection as _resolve_k3s_connection_fn, + _safe_str, +) from knoe.core.prod_config import ProdConfigApi, build_config, to_api_payload @@ -801,7 +797,10 @@ class ClusterScreenMixin: self._k3s_service_status_console = status_console self._k3s_service_deploy_console = deploy_console - self._common_services_success = bool(getattr(self, "_all_services_green", False)) + self._common_services_success = bool( + getattr(self, "_common_services_success", False) + or getattr(self, "_all_services_green", False) + ) self.update_footer() self._verify_k3s_services() @@ -845,7 +844,7 @@ class ClusterScreenMixin: return key def _is_valid_k8s_namespace(self, value: str | None) -> bool: - ns = (value or "").strip() + ns = _safe_str(value).strip() if not ns or len(ns) > 63: return False return re.fullmatch(r"[a-z0-9]([-a-z0-9]*[a-z0-9])?", ns) is not None diff --git a/knoe/ui/screens/database_options.py b/knoe/ui/screens/database_options.py index 57cbdcd..e757561 100644 --- a/knoe/ui/screens/database_options.py +++ b/knoe/ui/screens/database_options.py @@ -1,13 +1,13 @@ """Database Options screen for selecting distribution, version and extensions.""" -import json import os import re import threading -from pathlib import Path import tkinter as tk +from pathlib import Path from tkinter import ttk, messagebox +import json from knoe import screen as ui from knoe.core.build_context import copy_build_context_dir from knoe.core.env import ( @@ -18,6 +18,7 @@ from knoe.core.env import ( resolve_prole_home, ) + class DatabaseOptionsScreenMixin: """Mixin for the Database Options screen.""" diff --git a/knoe/ui/screens/deploy.py b/knoe/ui/screens/deploy.py index 2aa1d2b..19db267 100644 --- a/knoe/ui/screens/deploy.py +++ b/knoe/ui/screens/deploy.py @@ -2,12 +2,12 @@ import os import plistlib -import secrets import subprocess import threading -from pathlib import Path import tkinter as tk +from pathlib import Path from tkinter import ttk, messagebox, filedialog + from knoe import screen as ui from knoe.core.env import PROJECT_ROOT diff --git a/knoe/ui/screens/docker.py b/knoe/ui/screens/docker.py index 6e49a8a..98b5f99 100644 --- a/knoe/ui/screens/docker.py +++ b/knoe/ui/screens/docker.py @@ -1,7 +1,6 @@ """Docker image build, registry management and image pre-pull.""" import os -import platform import re import shutil import subprocess @@ -12,7 +11,6 @@ from pathlib import Path from knoe.config import get_docker_build_platform_args from knoe.core.env import ( PROJECT_ROOT, - _collect_images_from_files, _deployment_mode_from_env, _find_kubeconfig_file, _host_from_url, @@ -21,7 +19,6 @@ from knoe.core.env import ( _local_registry_enabled, _normalize_cluster_env, _push_docker_image, - _resolve_supabase_home, ) diff --git a/knoe/ui/screens/packaging.py b/knoe/ui/screens/packaging.py index fa82d43..44587d7 100644 --- a/knoe/ui/screens/packaging.py +++ b/knoe/ui/screens/packaging.py @@ -1,20 +1,20 @@ """Disk selection, installer packaging, DMG creation and build summary.""" import os -import platform import plistlib import shutil import subprocess import sys import threading import time +import tkinter as tk import webbrowser from pathlib import Path -import tkinter as tk from tkinter import ttk, messagebox, filedialog -from knoe.screen import TerminalConsole -from knoe import screen as ui + +import platform from knoe import config as inst_config +from knoe import screen as ui from knoe.core.env import PROJECT_ROOT, get_resource_path diff --git a/knoe/ui/screens/services.py b/knoe/ui/screens/services.py index 7e0a463..33e7f74 100644 --- a/knoe/ui/screens/services.py +++ b/knoe/ui/screens/services.py @@ -1,18 +1,17 @@ """Initialization scripts, CNPG deployment and service-layer overlays.""" import os -import platform -import secrets import shlex import subprocess import threading import time -import webbrowser -from pathlib import Path -from datetime import datetime import tkinter as tk +import webbrowser +from datetime import datetime +from pathlib import Path from tkinter import ttk, messagebox, filedialog -from knoe.screen import TerminalConsole + +import platform from knoe import screen as ui from knoe.core.env import ( PROJECT_ROOT, @@ -21,21 +20,21 @@ from knoe.core.env import ( _deployment_target_label, _normalize_cluster_env, ) -from knoe.core.policy import ( - POLICY_CFG_KEY, - OPTIONAL_WORKLOADS_MIN_READY_SCHEDULABLE_NODES, - evaluate_optional_workloads_allowed, -) +from knoe.core.ops import garage_store as garage_store_ops +from knoe.core.ops import monitoring as monitoring_ops +from knoe.core.ops import openbao as openbao_ops +from knoe.core.ops import opentofu as opentofu_ops +from knoe.core.ops import registry as registry_ops from knoe.core.ops.cloudnative_pg import ( initialize as cnpg_initialize, deploy as cnpg_deploy, rollout as cnpg_rollout, ) -from knoe.core.ops import openbao as openbao_ops -from knoe.core.ops import registry as registry_ops -from knoe.core.ops import garage_store as garage_store_ops -from knoe.core.ops import opentofu as opentofu_ops -from knoe.core.ops import monitoring as monitoring_ops +from knoe.core.policy import ( + POLICY_CFG_KEY, + OPTIONAL_WORKLOADS_MIN_READY_SCHEDULABLE_NODES, + evaluate_optional_workloads_allowed, +) class ServicesScreenMixin: diff --git a/knoe/ui/screens/supabase.py b/knoe/ui/screens/supabase.py index a3e81a4..3ce8e35 100644 --- a/knoe/ui/screens/supabase.py +++ b/knoe/ui/screens/supabase.py @@ -63,6 +63,54 @@ class SupabaseScreenMixin: self._overlay_widgets.append(enable_cb) y = 280 + node_label = ui.canvas_text( + self, + 48, + y, + "Node selector:", + fill="black", + font=("SF Pro Text", 12), + ) + self._canvas_items.append(node_label) + node_entry = tk.Entry( + self.bg_canvas, + textvariable=self.supabase_pv_node, + font=("SF Pro Text", 11), + width=38, + relief="solid", + bd=1, + ) + node_entry_window = self.bg_canvas.create_window( + 190, y - 2, window=node_entry, anchor="nw" + ) + self._canvas_items.append(node_entry_window) + self._overlay_widgets.append(node_entry) + + y += 36 + storage_label = ui.canvas_text( + self, + 48, + y, + "Storage directory:", + fill="black", + font=("SF Pro Text", 12), + ) + self._canvas_items.append(storage_label) + storage_entry = tk.Entry( + self.bg_canvas, + textvariable=self.supabase_pv_base_dir, + font=("SF Pro Text", 11), + width=38, + relief="solid", + bd=1, + ) + storage_entry_window = self.bg_canvas.create_window( + 190, y - 2, window=storage_entry, anchor="nw" + ) + self._canvas_items.append(storage_entry_window) + self._overlay_widgets.append(storage_entry) + + y += 42 initial_status = "Ready" if self.supabase_enabled.get() else "Disabled" self._supabase_status_var = tk.StringVar(value=initial_status) status_item = ui.canvas_text( @@ -86,10 +134,10 @@ class SupabaseScreenMixin: # Standardized Console Output self._supabase_console = self._create_console_output( - y=320, title="Deployment Output", width=880, height=380 + y=390, title="Deployment Output", width=880, height=310 ) - y = 740 + y = 720 # Deploy Supabase Button self._supabase_deploy_button = tk.Button( @@ -143,7 +191,27 @@ class SupabaseScreenMixin: env["PROLE_SERVICE"] = str(PROJECT_ROOT) env["NAMESPACE"] = namespace + pv_node = "" + try: + pv_node = (self.supabase_pv_node.get() or "").strip() + except Exception: + pv_node = "" + if pv_node: + env["SUPABASE_PV_NODE"] = pv_node + + pv_base_dir = "" + try: + pv_base_dir = (self.supabase_pv_base_dir.get() or "").strip() + except Exception: + pv_base_dir = "" + if pv_base_dir: + env["SUPABASE_PV_BASE_DIR"] = pv_base_dir + env["SUPABASE_PV_BASE"] = pv_base_dir + self._supabase_console.write("Starting Supabase deployment...\n") + self._supabase_console.write( + f"Placement: node={pv_node or 'auto'} storage={pv_base_dir or 'auto'}\n" + ) script_path = PROJECT_ROOT / "supabase" / "deploy.sh" if not script_path.exists(): @@ -191,6 +259,12 @@ class SupabaseScreenMixin: cfg_path = None if cfg_path: args.extend(["-c", str(cfg_path)]) + + try: + self._save_prole_cfg() + except Exception: + pass + if _parse_bool(os.environ.get("SUPABASE_USE_DEV_COMPOSE"), False): args.append("--with-dev-helpers") if _parse_bool(os.environ.get("SUPABASE_FOREGROUND"), False): diff --git a/knoe/ui/screens/validate.py b/knoe/ui/screens/validate.py index 744961b..8577c5f 100644 --- a/knoe/ui/screens/validate.py +++ b/knoe/ui/screens/validate.py @@ -3,10 +3,10 @@ import subprocess import threading import time -import webbrowser import tkinter as tk +import webbrowser from tkinter import ttk, messagebox, filedialog -from knoe.screen import TerminalConsole + from knoe import screen as ui diff --git a/modes/k3s/knoe-db/.version b/modes/k3s/knoe-db/.version index 4701cc7..147ea53 100644 --- a/modes/k3s/knoe-db/.version +++ b/modes/k3s/knoe-db/.version @@ -1 +1 @@ -150 \ No newline at end of file +158 \ No newline at end of file diff --git a/prole/__init__.py b/prole/__init__.py index f5b1554..685dacf 100644 --- a/prole/__init__.py +++ b/prole/__init__.py @@ -1,3 +1,3 @@ -from prole.deployment import ProleDeployment +from prole.deployment import KnoeDeployment -__all__ = ["ProleDeployment"] +__all__ = ["KnoeDeployment"] diff --git a/prole/cli.py b/prole/cli.py index dfa480a..42567bc 100644 --- a/prole/cli.py +++ b/prole/cli.py @@ -1,14 +1,13 @@ from __future__ import annotations import argparse -import logging import os import sys from pathlib import Path -from knoe.core.controller import ProleController +from knoe.core.actions import KnoeConsoleInstaller, _prepare_k3s_pipeline +from knoe.core.controller import KnoeController from knoe.core.env import _ensure_ansible_vault_credentials, PROJECT_ROOT -from knoe.core.actions import ProleConsoleInstaller, _prepare_k3s_pipeline def main(): @@ -44,6 +43,12 @@ def main(): action="store_true", help="Reset k3d dev cluster or k3s service cluster", ) + parser.add_argument( + "--delete-db", + action="store_true", + default=False, + help=argparse.SUPPRESS, + ) parser.add_argument( "--no-gui", action="store_true", @@ -63,6 +68,9 @@ def main(): prole_conf = os.environ.get("PROLE_CONF") args = parser.parse_args() + if args.delete_db and not args.reset: + parser.error("--delete-db requires --reset") + if args.config is None and prole_conf: potential_cfg = Path(prole_conf) / "prole.cfg" if potential_cfg.exists(): @@ -72,16 +80,16 @@ def main(): setup_logging(verbose=args.verbose, debug=args.debug) - controller = ProleController( + controller = KnoeController( PROJECT_ROOT, verbose=args.verbose, cfg_path=Path(args.config).expanduser() if args.config else None, ) if args.command == "deploy": - from prole.deployment import ProleDeployment + from prole.deployment import KnoeDeployment - deployment = ProleDeployment(controller, PROJECT_ROOT) + deployment = KnoeDeployment(controller, PROJECT_ROOT) if deployment.duplicate_k3d_to_k3s(): if deployment.apply(): sys.exit(0) @@ -89,8 +97,11 @@ def main(): if args.silent: _ensure_ansible_vault_credentials(prompt_ui=False) - installer = ProleConsoleInstaller( - controller, args.config, reset_cluster=args.reset + installer = KnoeConsoleInstaller( + controller, + args.config, + reset_cluster=args.reset, + delete_db=args.delete_db, ) if args.log: log_path = ( diff --git a/prole/deployment.py b/prole/deployment.py index 298d8e5..8bf8643 100644 --- a/prole/deployment.py +++ b/prole/deployment.py @@ -1,17 +1,16 @@ from __future__ import annotations import configparser -import logging import os import subprocess from pathlib import Path +import logging from knoe import prole_conf -from knoe.core.actions import ProleInstaller +from knoe.core.actions import KnoeInstaller from knoe.core.env import ( _detect_ansible_topology, _sync_opentofu_pipeline, - _normalize_cluster_env, _parse_bool, ) from knoe.core.ops.cloudnative_pg import initialize as cnpg_initialize @@ -40,7 +39,7 @@ _CFG_KEY_MAP = { } -class ProleDeployment(ProleInstaller): +class KnoeDeployment(KnoeInstaller): """ Top-level class for Prole infrastructure deployment via OpenTofu. Duplicate k3d stack and apply to k3s defined in Ansible inventory. diff --git a/scan/network_description.txt b/scan/network_description.txt index 745348f..4b2b53b 100644 --- a/scan/network_description.txt +++ b/scan/network_description.txt @@ -1,58 +1,53 @@ Network Discovery Summary: Primary Router: 10.0.0.1 (eero_5d:50:f2) -DNS Servers: 10.0.0.4, 100.100.100.100, 10.0.0.5 +DNS Servers: 10.0.0.4, 10.0.0.5, 100.100.100.100 Detected Devices: - 10.0.0.35 [7e:19:9a:fe:9f:ed] (7e:19:9a:fe:9f:ed): Ports [22, 445, 5900, 11434, 88], Services: ['SSH', 'VNC', 'SMB/CIFS (Possible Windows/AD)', 'Ollama', 'Active Directory Related'] -- 10.0.0.1 [9c:57:bc:5d:50:f2] (eero_5d:50:f2): Ports [53], Services: ['DNS'] - 10.0.0.203 [00:11:32:3b:2f:08] (Synology_3b:2f:08): Ports [22, 80, 443, 2049, 445], Services: ['SSH', 'NFS', 'Web Server', 'SMB/CIFS (Possible Windows/AD)'] -- 10.0.0.3 [2c:cf:67:8c:2b:47] (RaspberryPi_8c:2b:47): Ports [22, 53, 80, 443, 2049, 445, 5900, 88, 389, 636], Services: ['DNS', 'SSH', 'NFS', 'VNC', 'Web Server', 'SMB/CIFS (Possible Windows/AD)', 'Active Directory Related'] +- 10.0.0.35\ [7e:19:9a:fe:9f:ed] (7e:19:9a:fe:9f:ed): Ports [], Services: [] +- 10.0.0.3 [2c:cf:67:8c:2b:47] (RaspberryPi_8c:2b:47): Ports [22, 80, 2049, 445, 5900, 88, 389, 636], Services: ['SSH', 'NFS', 'VNC', 'Web Server', 'SMB/CIFS (Possible Windows/AD)', 'Active Directory Related'] +- 10.0.0.1 [9c:57:bc:5d:50:f2] (eero_5d:50:f2): Ports [], Services: [] +- 10.0.0.4 [b8:27:eb:b3:6f:2b] (RaspberryPiF_b3:6f:2b): Ports [22, 53, 443], Services: ['DNS', 'SSH', 'Web Server'] - 10.0.0.5 [b8:27:eb:88:3a:41] (RaspberryPiF_88:3a:41): Ports [22, 2049, 5900], Services: ['SSH', 'NFS', 'VNC'] -- 10.0.0.4 [b8:27:eb:b3:6f:2b] (RaspberryPiF_b3:6f:2b): Ports [22, 53, 443, 2049], Services: ['DNS', 'SSH', 'NFS', 'Web Server'] -- 10.0.0.6 [2c:cf:67:45:8d:97] (RaspberryPi_45:8d:97): Ports [22, 80, 443], Services: ['SSH', 'Web Server'] - 10.0.0.32 [48:a6:b8:a4:8e:cc] (Sonos_a4:8e:cc): Ports [], Services: [] -- 10.0.0.7 [2c:cf:67:ca:93:ce] (RaspberryPi_ca:93:ce): Ports [22], Services: ['SSH'] -- 10.0.0.2 [dc:a4:ca:ea:1a:2f] (Apple_ea:1a:2f): Ports [], Services: [] -- 10.0.0.22 [2c:cf:67:45:8d:97] (RaspberryPi_45:8d:97): Ports [22, 80, 443], Services: ['SSH', 'Web Server'] - 10.0.0.26 [c8:db:26:08:52:f8] (Logitech_08:52:f8): Ports [], Services: [] -- 10.0.0.204 [50:eb:f6:56:f3:26] (ASUSTekCOMPU_56:f3:26): Ports [22, 3389, 445, 5900, 11434], Services: ['SSH', 'VNC', 'SMB/CIFS (Possible Windows/AD)', 'RDP (Windows)', 'Ollama'] -- 10.0.0.33 [ec:b5:fa:b0:76:e4] (PhilipsLight_b0:76:e4): Ports [80, 443], Services: ['Web Server'] -- 10.0.0.34 [3a:b4:ce:79:b0:55] (3a:b4:ce:79:b0:55): Ports [], Services: [] +- 10.0.0.6 [2c:cf:67:45:8d:97] (RaspberryPi_45:8d:97): Ports [22, 443], Services: ['SSH', 'Web Server'] +- 10.0.0.2 [dc:a4:ca:ea:1a:2f] (Apple_ea:1a:2f): Ports [], Services: [] +- 10.0.0.7 [2c:cf:67:ca:93:ce] (RaspberryPi_ca:93:ce): Ports [22], Services: ['SSH'] +- 10.0.0.22 [2c:cf:67:45:8d:97] (RaspberryPi_45:8d:97): Ports [22], Services: ['SSH'] +- 10.0.0.33 [ec:b5:fa:b0:76:e4] (PhilipsLight_b0:76:e4): Ports [443], Services: ['Web Server'] +- 10.0.0.48 [0c:fe:45:53:f4:3c] (SonyInteract_53:f4:3c): Ports [], Services: [] - 10.0.0.37 [54:07:7d:22:c0:b2] (Netgear_22:c0:b2): Ports [80], Services: ['Web Server'] - 10.0.0.45 [f8:b4:6a:30:a4:1f] (HewlettPacka_30:a4:1f): Ports [80, 443], Services: ['Web Server'] -- 10.0.0.48 [0c:fe:45:53:f4:3c] (SonyInteract_53:f4:3c): Ports [], Services: [] -- 10.0.0.52 [2c:cf:67:ca:93:ce] (RaspberryPi_ca:93:ce): Ports [22], Services: ['SSH'] -- 10.0.0.205 [a8:20:66:28:12:e7] (Apple_28:12:e7): Ports [22, 53, 445, 5900, 88], Services: ['DNS', 'SSH', 'VNC', 'SMB/CIFS (Possible Windows/AD)', 'Active Directory Related'] - 10.0.0.46 [d4:f7:d5:40:ab:17] (SonyInteract_40:ab:17): Ports [], Services: [] -- 10.0.0.41 [b8:27:eb:57:10:d9] (RaspberryPiF_57:10:d9): Ports [22], Services: ['SSH'] -- 10.0.0.107 [b8:06:0d:b7:7c:56] (TuyaSmart_b7:7c:56): Ports [], Services: [] -- 10.0.0.100 [24:fc:e5:51:cf:74] (SamsungElect_51:cf:74): Ports [], Services: [] -- 10.0.0.189 [00:17:88:a3:2f:cc] (PhilipsLight_a3:2f:cc): Ports [80, 443], Services: ['Web Server'] +- 10.0.0.52 [2c:cf:67:ca:93:ce] (RaspberryPi_ca:93:ce): Ports [], Services: [] +- 10.0.0.41 [b8:27:eb:57:10:d9] (RaspberryPiF_57:10:d9): Ports [], Services: [] - 10.0.0.73 [4c:a9:19:b3:12:f8] (TuyaSmart_b3:12:f8): Ports [], Services: [] -- 10.0.0.206 [00:a0:de:a2:0b:ef] (Yamaha_a2:0b:ef): Ports [80], Services: ['Web Server'] -- 10.0.0.94 [f0:20:ff:bf:9a:19] (Intel_bf:9a:19): Ports [], Services: [] - Note: Potential host for NVIDIA cards (PC Hardware vendor) -- 10.0.0.95 [b8:27:eb:88:3a:41] (RaspberryPiF_88:3a:41): Ports [22, 2049, 5900], Services: ['SSH', 'NFS', 'VNC'] +- 10.0.0.100 [24:fc:e5:51:cf:74] (SamsungElect_51:cf:74): Ports [], Services: [] +- 10.0.0.95 [b8:27:eb:88:3a:41] (RaspberryPiF_88:3a:41): Ports [2049], Services: ['NFS'] - 10.0.0.106 [48:a6:b8:a7:50:60] (Sonos_a7:50:60): Ports [], Services: [] - 10.0.0.99 [20:28:bc:f1:dc:ba] (Visionscape_f1:dc:ba): Ports [], Services: [] +- 10.0.0.107 [b8:06:0d:b7:7c:56] (TuyaSmart_b7:7c:56): Ports [], Services: [] - 10.0.0.111 [ca:0b:6c:5d:0a:e5] (ca:0b:6c:5d:0a:e5): Ports [], Services: [] +- 10.0.0.113 [60:81:10:92:02:4e] (Apple_92:02:4e): Ports [], Services: [] - 10.0.0.123 [3c:ef:8c:96:e3:3c] (ZhejiangDahu_96:e3:3c): Ports [80], Services: ['Web Server'] - 10.0.0.124 [9c:8e:cd:02:73:c4] (AmcrestTechn_02:73:c4): Ports [80], Services: ['Web Server'] -- 10.0.0.112 [f8:33:31:40:58:c0] (TexasInstrum_40:58:c0): Ports [80, 443], Services: ['Web Server'] -- 10.0.0.117 [40:f5:20:1e:5e:91] (Espressif_1e:5e:91): Ports [80], Services: ['Web Server'] - 10.0.0.127 [58:55:ca:4a:60:6e] (Apple_4a:60:6e): Ports [22], Services: ['SSH'] -- 10.0.0.125 [a0:60:32:04:73:ba] (AmcrestTechn_04:73:ba): Ports [80], Services: ['Web Server'] +- 10.0.0.117 [40:f5:20:1e:5e:91] (Espressif_1e:5e:91): Ports [80], Services: ['Web Server'] +- 10.0.0.125 [a0:60:32:04:73:ba] (AmcrestTechn_04:73:ba): Ports [], Services: [] - 10.0.0.130 [f8:bb:bf:a3:84:eb] (eero_a3:84:eb): Ports [53], Services: ['DNS'] - 10.0.0.143 [60:5f:8d:7a:aa:32] (eero_7a:aa:32): Ports [53], Services: ['DNS'] +- 10.0.0.205 [a8:20:66:28:12:e7] (Apple_28:12:e7): Ports [22, 53, 445, 5900, 88], Services: ['DNS', 'SSH', 'VNC', 'SMB/CIFS (Possible Windows/AD)', 'Active Directory Related'] +- 10.0.0.128 [a8:20:66:28:12:e7] (Apple_28:12:e7): Ports [], Services: [] - 10.0.0.145 [7c:a6:b0:04:db:1c] (7c:a6:b0:04:db:1c): Ports [], Services: [] -- 10.0.0.175 [28:80:88:e4:10:1a] (Netgear_e4:10:1a): Ports [80], Services: ['Web Server'] -- 10.0.0.155 [62:17:97:b3:06:a1] (62:17:97:b3:06:a1): Ports [], Services: [] -- 10.0.0.160 [96:c3:fd:58:02:6c] (96:c3:fd:58:02:6c): Ports [], Services: [] - 10.0.0.170 [b0:ee:7b:ca:73:99] (Roku_ca:73:99): Ports [], Services: [] -- 10.0.0.188 [00:16:6c:c5:1f:54] (SamsungElect_c5:1f:54): Ports [80, 443], Services: ['Web Server'] +- 10.0.0.175 [28:80:88:e4:10:1a] (Netgear_e4:10:1a): Ports [80], Services: ['Web Server'] +- 10.0.0.189 [00:17:88:a3:2f:cc] (PhilipsLight_a3:2f:cc): Ports [80, 443], Services: ['Web Server'] - 10.0.0.196 [60:5f:8d:88:08:f2] (eero_88:08:f2): Ports [53], Services: ['DNS'] -- 10.0.0.58 [7e:46:74:2a:b0:b2] (7e:46:74:2a:b0:b2): Ports [], Services: [] -- 10.0.0.199 [7c:a6:b0:01:a7:33] (7c:a6:b0:01:a7:33): Ports [], Services: [] -- 10.0.0.60 [c6:d0:1a:97:9f:e9] (c6:d0:1a:97:9f:e9): Ports [], Services: [] +- 10.0.0.188 [00:16:6c:c5:1f:54] (SamsungElect_c5:1f:54): Ports [80, 443], Services: ['Web Server'] +- 10.0.0.204 [50:eb:f6:56:f3:26] (ASUSTekCOMPU_56:f3:26): Ports [22, 3389, 11434], Services: ['SSH', 'RDP (Windows)', 'Ollama'] +- 10.0.0.206 [00:a0:de:a2:0b:ef] (Yamaha_a2:0b:ef): Ports [80], Services: ['Web Server'] +- 10.0.0.1\ [9c:57:bc:5d:50:f2] (eero_5d:50:f2): Ports [], Services: [] Ollama Instances found at: 10.0.0.35, 10.0.0.204 Ansible Inventory Summary: diff --git a/supabase/helm/prole-supabase/templates/storage/deployment.yaml b/supabase/helm/prole-supabase/templates/storage/deployment.yaml index b2add45..e0578e2 100644 --- a/supabase/helm/prole-supabase/templates/storage/deployment.yaml +++ b/supabase/helm/prole-supabase/templates/storage/deployment.yaml @@ -90,7 +90,8 @@ spec: image: "{{ .Values.image.storage.repository }}:{{ .Values.image.storage.tag | default .Chart.AppVersion }}" imagePullPolicy: {{ .Values.image.storage.pullPolicy }} env: - {{- range $key, $value := .Values.environment.storage }} + {{- $storageEnv := (.Values.environment.storage | default dict) }} + {{- range $key, $value := $storageEnv }} {{- if and (ne $key "DB_HOST") (ne $key "DB_PORT") (ne $key "DB_SSL") }} - name: {{ $key }} value: {{ $value | quote }} @@ -193,16 +194,24 @@ spec: value: {{ printf "http://%s:%v" (include "supabase.imgproxy.fullname" .) (.Values.service.imgproxy.port | int) | quote }} {{- end }} + {{- if not (hasKey $storageEnv "STORAGE_BACKEND") }} - name: STORAGE_BACKEND value: {{ if .Values.deployment.minio.enabled }}s3{{ else }}file{{ end }} + {{- end }} {{- if .Values.deployment.minio.enabled }} + {{- if not (hasKey $storageEnv "GLOBAL_S3_ENDPOINT") }} - name: GLOBAL_S3_ENDPOINT value: {{ printf "http://%s:%v" (include "supabase.minio.fullname" .) (default 9000 .Values.service.minio.port) | quote }} + {{- end }} + {{- if not (hasKey $storageEnv "GLOBAL_S3_PROTOCOl") }} - name: GLOBAL_S3_PROTOCOl value: http + {{- end }} + {{- if not (hasKey $storageEnv "GLOBAL_S3_FORCE_PATH_STYLE") }} - name: GLOBAL_S3_FORCE_PATH_STYLE value: "true" + {{- end }} - name: AWS_ACCESS_KEY_ID valueFrom: secretKeyRef: diff --git a/supabase/helm/render_supabase.py b/supabase/helm/render_supabase.py index a30bd97..fb26794 100755 --- a/supabase/helm/render_supabase.py +++ b/supabase/helm/render_supabase.py @@ -67,6 +67,45 @@ def _is_placeholder(value: str) -> bool: return v.startswith("${") and v.endswith("}") +def _as_bool(value: str | bool | None, default: bool = False) -> bool: + if isinstance(value, bool): + return value + if value is None: + return default + normalized = str(value).strip().lower() + if not normalized: + return default + if normalized in {"1", "true", "yes", "on", "y"}: + return True + if normalized in {"0", "false", "no", "off", "n"}: + return False + return default + + +def _parse_hosts(raw_hosts: str, fallback: str = "db.prole.org") -> list[str]: + entries: list[str] = [] + seen: set[str] = set() + for entry in (raw_hosts or "").split(","): + token = (entry or "").strip() + if not token or _is_placeholder(token): + continue + + if "://" in token: + parsed = urllib.parse.urlparse(token) + host = (parsed.hostname or "").strip() + else: + host = token + + if not host or host in seen: + continue + seen.add(host) + entries.append(host) + + if entries: + return entries + return [fallback] + + def _discover_k8s_service_namespace(service_name: str) -> str: try: raw = subprocess.check_output(["kubectl", "get", "svc", "-A", "-o", "json"], text=True) @@ -161,7 +200,9 @@ def _build_overlay(cfg: configparser.ConfigParser, args: argparse.Namespace) -> anon_key = _jwt(anon_payload, jwt_secret) service_key = _jwt(service_payload, jwt_secret) - supabase_hostname = _first( + supabase_hostname_raw = _first( + os.environ.get("SUPABASE_STUDIO_URL", ""), + _cfg_get(cfg, "Inputs", "init_cluster.supabase_studio_url"), _cfg_get(cfg, "User", "supabase_hostname"), _cfg_get(cfg, "Global", "supabase_hostname"), _cfg_get(cfg, "User", "SUPABASE_HOSTNAME"), @@ -171,16 +212,73 @@ def _build_overlay(cfg: configparser.ConfigParser, args: argparse.Namespace) -> default="db.prole.org", ).strip() - if _is_placeholder(supabase_hostname): - supabase_hostname = "" - if not supabase_hostname: - supabase_hostname = "db.prole.org" + if _is_placeholder(supabase_hostname_raw): + supabase_hostname_raw = "" + if not supabase_hostname_raw: + supabase_hostname_raw = "db.prole.org" + + studio_enabled = _as_bool( + _first( + os.environ.get("SUPABASE_STUDIO_ENABLED", ""), + _cfg_get(cfg, "Inputs", "init_cluster.supabase_studio_enabled"), + _cfg_get(cfg, "Global", "SUPABASE_STUDIO_ENABLED"), + default="true", + ), + default=True, + ) + auth_enabled = _as_bool( + _first( + os.environ.get("SUPABASE_AUTH_ENABLED", ""), + _cfg_get(cfg, "Inputs", "init_cluster.supabase_auth_enabled"), + _cfg_get(cfg, "Global", "SUPABASE_AUTH_ENABLED"), + default="true", + ), + default=True, + ) + realtime_enabled = _as_bool( + _first( + os.environ.get("SUPABASE_REALTIME_ENABLED", ""), + _cfg_get(cfg, "Inputs", "init_cluster.supabase_realtime_enabled"), + _cfg_get(cfg, "Global", "SUPABASE_REALTIME_ENABLED"), + default="true", + ), + default=True, + ) + meta_enabled = _as_bool( + _first( + os.environ.get("SUPABASE_META_ENABLED", ""), + _cfg_get(cfg, "Inputs", "init_cluster.supabase_meta_enabled"), + _cfg_get(cfg, "Global", "SUPABASE_META_ENABLED"), + default="true", + ), + default=True, + ) + analytics_enabled = _as_bool( + _first( + os.environ.get("SUPABASE_ANALYTICS_ENABLED", ""), + _cfg_get(cfg, "Inputs", "init_cluster.supabase_analytics_enabled"), + _cfg_get(cfg, "Global", "SUPABASE_ANALYTICS_ENABLED"), + default="true", + ), + default=True, + ) + + raw_host_entries: list[str] = [] + for entry in supabase_hostname_raw.split(","): + token = (entry or "").strip() + if token and not _is_placeholder(token): + raw_host_entries.append(token) + if not raw_host_entries: + raw_host_entries = ["db.prole.org"] + + host_entries = _parse_hosts(",".join(raw_host_entries), fallback="db.prole.org") + ingress_host = host_entries[0] # Canonical public URL for Supabase (used for auth callbacks + Studio links). public_url = "" - ingress_host = supabase_hostname - if "://" in supabase_hostname: - parsed = urllib.parse.urlparse(supabase_hostname) + primary_raw_host = (raw_host_entries[0] or "").strip() + if "://" in primary_raw_host: + parsed = urllib.parse.urlparse(primary_raw_host) ingress_host = (parsed.hostname or "").strip() or ingress_host scheme = (parsed.scheme or "https").strip() if parsed.netloc: @@ -220,9 +318,13 @@ def _build_overlay(cfg: configparser.ConfigParser, args: argparse.Namespace) -> ).strip().lower() supabase_primary_node = _first( os.environ.get("SUPABASE_PRIMARY_NODE", ""), + os.environ.get("SUPABASE_NODE_SELECTOR", ""), + os.environ.get("SUPABASE_PV_NODE", ""), _cfg_get(cfg, "Global", "SUPABASE_PRIMARY_NODE"), + _cfg_get(cfg, "Global", "SUPABASE_NODE_SELECTOR"), + _cfg_get(cfg, "Global", "SUPABASE_PV_NODE"), _cfg_get(cfg, "Supabase", "PRIMARY_NODE"), - default="merlin.prole.org", + default="gandalf.prole.org", ).strip() if _is_placeholder(supabase_primary_node): supabase_primary_node = "" @@ -233,7 +335,7 @@ def _build_overlay(cfg: configparser.ConfigParser, args: argparse.Namespace) -> os.environ.get("SUPABASE_STORAGE_CLASS", ""), _cfg_get(cfg, "Global", "SUPABASE_STORAGE_CLASS"), _cfg_get(cfg, "Supabase", "STORAGE_CLASS"), - default="merlin-local-iscsi-d002", + default="synology-iscsi", ).strip() if _is_placeholder(supabase_storage_class): supabase_storage_class = "merlin-local-iscsi-d002" @@ -243,20 +345,20 @@ def _build_overlay(cfg: configparser.ConfigParser, args: argparse.Namespace) -> "fullnameOverride": "supabase", "deployment": { "db": {"enabled": False}, - "functions": {"enabled": False}, - "vector": {"enabled": False}, - "kong": {"enabled": False}, - "storage": {"enabled": False}, - "minio": {"enabled": False}, - "imgproxy": {"enabled": False}, + "functions": {"enabled": True}, + "vector": {"enabled": True}, + "kong": {"enabled": True}, + "storage": {"enabled": True}, + "minio": {"enabled": True}, + "imgproxy": {"enabled": True}, # Explicit fullnameOverride per component strips the chart name # from pod names (avoids 'supabase-prole-supabase-'). - "analytics": {"fullnameOverride": "supabase-analytics"}, - "auth": {"fullnameOverride": "supabase-auth"}, - "meta": {"fullnameOverride": "supabase-meta"}, - "realtime": {"fullnameOverride": "supabase-realtime"}, - "rest": {"fullnameOverride": "supabase-rest"}, - "studio": {"fullnameOverride": "supabase-studio"}, + "analytics": {"enabled": analytics_enabled, "fullnameOverride": "supabase-analytics"}, + "auth": {"enabled": auth_enabled, "fullnameOverride": "supabase-auth"}, + "meta": {"enabled": meta_enabled, "fullnameOverride": "supabase-meta"}, + "realtime": {"enabled": realtime_enabled, "fullnameOverride": "supabase-realtime"}, + "rest": {"enabled": True, "fullnameOverride": "supabase-rest"}, + "studio": {"enabled": studio_enabled, "fullnameOverride": "supabase-studio"}, }, "externalDatabase": { "enabled": True, @@ -287,9 +389,12 @@ def _build_overlay(cfg: configparser.ConfigParser, args: argparse.Namespace) -> "ingress": { "enabled": True, "className": "traefik", - "hosts": [ - {"host": ingress_host, "paths": [{"path": "/", "pathType": "Prefix"}]} - ], + "hosts": [{"host": host, "paths": [{"path": "/", "pathType": "Prefix"}]} for host in host_entries], + }, + "studioIngress": { + "enabled": True, + "className": "traefik", + "hosts": [{"host": host, "paths": [{"path": "/", "pathType": "Prefix"}]} for host in host_entries], }, } @@ -304,20 +409,24 @@ def _build_overlay(cfg: configparser.ConfigParser, args: argparse.Namespace) -> ) # Write to persistence.*.storageClassName — the path the Helm chart actually uses persistence = overlay.setdefault("persistence", {}) - # Only set storageClass for components that are still deployed; storage/minio/imgproxy are removed. - for pvc_key in ("functions", "snippets", "deno"): + for pvc_key in ("functions", "snippets", "deno", "imgproxy", "minio", "storage"): persistence.setdefault(pvc_key, {})["storageClassName"] = supabase_storage_class if mode == "k3s" and supabase_node_selector: deployment = overlay.setdefault("deployment", {}) - # Only pin active control-plane components; storage/minio/imgproxy/kong are disabled. for component in ( "analytics", "auth", + "functions", + "imgproxy", + "kong", "meta", + "minio", "realtime", "rest", + "storage", "studio", + "vector", ): component_cfg = deployment.setdefault(component, {}) component_cfg.setdefault("nodeSelector", {}) diff --git a/tests/installer/test_actions_env_setup_expands_shell_vars.py b/tests/installer/test_actions_env_setup_expands_shell_vars.py index 06e48b5..ef98724 100644 --- a/tests/installer/test_actions_env_setup_expands_shell_vars.py +++ b/tests/installer/test_actions_env_setup_expands_shell_vars.py @@ -23,9 +23,9 @@ def test_actions_save_env_to_file_expands_shell_vars(monkeypatch, tmp_path: Path ) (proj / "etc" / "dummy.txt").write_text("ok\n") - from knoe.core.actions import ProleInstaller + from knoe.core.actions import KnoeInstaller - class Dummy(ProleInstaller): + class Dummy(KnoeInstaller): def __init__(self, project_root: Path): self.project_root = project_root self.inputs = {"init_cluster.cluster_env": "dev"} diff --git a/tests/installer/test_actions_helpers.py b/tests/installer/test_actions_helpers.py index 55d4f6d..6033ae8 100644 --- a/tests/installer/test_actions_helpers.py +++ b/tests/installer/test_actions_helpers.py @@ -1,6 +1,7 @@ """Tests for installer/core/actions.py – ProleInstallerBase helper methods.""" from __future__ import annotations + import os import subprocess import sys @@ -8,11 +9,9 @@ from pathlib import Path from unittest import mock from unittest.mock import MagicMock, patch -import pytest - from knoe.core.actions import ( - ProleInstaller, - ProleConsoleInstaller, + KnoeInstaller, + KnoeConsoleInstaller, _configure_unbuffered_io, _select_existing_kubeconfig, ) @@ -23,7 +22,7 @@ from knoe.core.actions import ( # --------------------------------------------------------------------------- -class _TestableInstaller(ProleInstaller): +class _TestableInstaller(KnoeInstaller): """Minimal concrete subclass for testing base-class helpers.""" def __init__(self, inputs=None, project_root=None): @@ -329,8 +328,8 @@ spec: # --------------------------------------------------------------------------- -class _TestableSilentInstaller(ProleConsoleInstaller): - """Testable subclass of ProleConsoleInstaller.""" +class _TestableSilentInstaller(KnoeConsoleInstaller): + """Testable subclass of KnoeConsoleInstaller.""" def __init__(self, inputs=None, project_root=None): self._inputs_dict = inputs or {} diff --git a/tests/installer/test_config.py b/tests/installer/test_config.py index d8a887b..018e224 100644 --- a/tests/installer/test_config.py +++ b/tests/installer/test_config.py @@ -1,5 +1,5 @@ -import pytest from unittest.mock import patch, MagicMock + from knoe.config import ( normalize_version, is_apple_silicon, diff --git a/tests/installer/test_config_extended.py b/tests/installer/test_config_extended.py index bc415ac..5562d83 100644 --- a/tests/installer/test_config_extended.py +++ b/tests/installer/test_config_extended.py @@ -1,15 +1,13 @@ """Extended tests for installer/config.py – covering encryption, properties, helpers.""" from __future__ import annotations + import configparser import os -import platform from pathlib import Path from unittest import mock from unittest.mock import MagicMock, patch -import pytest - from knoe.config import ( _is_prole_secret, _is_openbao_ref, diff --git a/tests/installer/test_config_helpers.py b/tests/installer/test_config_helpers.py index e9c4d41..8a0c6f1 100644 --- a/tests/installer/test_config_helpers.py +++ b/tests/installer/test_config_helpers.py @@ -13,7 +13,6 @@ import os import subprocess from pathlib import Path from unittest.mock import MagicMock, patch -import pytest from knoe.config import ( _get_secret_key_file, diff --git a/tests/installer/test_controller.py b/tests/installer/test_controller.py index 2f349d1..5f7e06e 100644 --- a/tests/installer/test_controller.py +++ b/tests/installer/test_controller.py @@ -1,7 +1,7 @@ """ Unit tests for installer/core/controller.py -Covers ProleController: check_docker_running, get_knoe_db_version, run_script. +Covers KnoeController: check_docker_running, get_knoe_db_version, run_script. """ from __future__ import annotations @@ -9,17 +9,16 @@ import os import subprocess from pathlib import Path from unittest.mock import MagicMock, patch -import pytest -from knoe.core.controller import ProleController +from knoe.core.controller import KnoeController # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- -def _make_controller(tmp_path: Path, **kwargs) -> ProleController: - return ProleController(project_root=tmp_path, **kwargs) +def _make_controller(tmp_path: Path, **kwargs) -> KnoeController: + return KnoeController(project_root=tmp_path, **kwargs) def _scaffold_project(tmp_path: Path) -> None: diff --git a/tests/installer/test_controller_run_script_expands_shell_vars.py b/tests/installer/test_controller_run_script_expands_shell_vars.py index 50ba64e..80e10c5 100644 --- a/tests/installer/test_controller_run_script_expands_shell_vars.py +++ b/tests/installer/test_controller_run_script_expands_shell_vars.py @@ -20,9 +20,9 @@ def test_controller_run_script_expands_shell_vars(monkeypatch, tmp_path: Path): (proj / "etc").mkdir(parents=True, exist_ok=True) (proj / "etc" / "dummy.sh").write_text("#!/usr/bin/env bash\necho ok\n") - from knoe.core.controller import ProleController + from knoe.core.controller import KnoeController - controller = ProleController(project_root=proj) + controller = KnoeController(project_root=proj) rc = controller.run_script("dummy.sh", env={"PROLE_HOME": "$HOME/prole"}) assert rc == 0 diff --git a/tests/installer/test_core_classes.py b/tests/installer/test_core_classes.py index 862c3c4..3af2c32 100644 --- a/tests/installer/test_core_classes.py +++ b/tests/installer/test_core_classes.py @@ -6,13 +6,11 @@ import os from pathlib import Path from unittest import mock -import pytest - -from knoe.state import InstallerState -from knoe.milestone import Milestone, ProgressCallback from knoe.build import get_build_command, BuildMilestone -from knoe.core.controller import ProleController -from knoe.core.actions import ProleConsoleInstaller +from knoe.core.actions import KnoeConsoleInstaller +from knoe.core.controller import KnoeController +from knoe.milestone import Milestone +from knoe.state import InstallerState # ===== InstallerState ===== @@ -262,31 +260,31 @@ class TestBuildMilestone: assert m.next(InstallerState()) is None -# ===== ProleController ===== +# ===== KnoeController ===== -class TestProleController: +class TestKnoeController: def test_init(self, tmp_path): - c = ProleController(tmp_path) + c = KnoeController(tmp_path) assert c.project_root == tmp_path assert c.verbose is False assert c.state is not None - installer = ProleConsoleInstaller(c) + installer = KnoeConsoleInstaller(c) defaults = installer._default_inputs() assert defaults.get("kerberos_config.user") == "administrator" assert defaults.get("kerberos_config.password") == "" def test_init_verbose(self, tmp_path): - c = ProleController(tmp_path, verbose=True) + c = KnoeController(tmp_path, verbose=True) assert c.verbose is True def test_check_docker_running(self, tmp_path): - c = ProleController(tmp_path) + c = KnoeController(tmp_path) result = c.check_docker_running() assert isinstance(result, bool) def test_get_knoe_db_version_defaults(self, tmp_path): - c = ProleController(tmp_path) + c = KnoeController(tmp_path) version = c.get_knoe_db_version() assert "17.7" in version # default pg version assert "-" in version @@ -298,12 +296,12 @@ class TestProleController: db_dir = tmp_path / "knoe-db" db_dir.mkdir() (db_dir / ".version").write_text("42") - c = ProleController(tmp_path) + c = KnoeController(tmp_path) version = c.get_knoe_db_version() assert version == "16.2-042" def test_run_milestones(self, tmp_path): - c = ProleController(tmp_path) + c = KnoeController(tmp_path) m1 = _DummyMilestone("step1", "Step 1") m2 = _DummyMilestone("step2", "Step 2") msgs = [] @@ -312,21 +310,21 @@ class TestProleController: assert "step2" in c.state.completed def test_run_milestones_no_callback(self, tmp_path): - c = ProleController(tmp_path) + c = KnoeController(tmp_path) m = _DummyMilestone("s1", "S1") c.run_milestones([m]) assert "s1" in c.state.completed def test_run_script_missing(self, tmp_path): - c = ProleController(tmp_path) + c = KnoeController(tmp_path) # Script doesn't exist but should not crash rc = c.run_script("nonexistent_script.sh", args=[]) # Will fail since the script doesn't exist assert rc != 0 or rc is None def test_silent_installer_runs_supabase_preload_before_deploy(self, tmp_path): - c = ProleController(tmp_path) - installer = ProleConsoleInstaller(c) + c = KnoeController(tmp_path) + installer = KnoeConsoleInstaller(c) with mock.patch.object(installer, "_load_inputs_from_cfg", return_value={}), \ mock.patch.object(installer, "_write_cfg"), \ @@ -348,8 +346,8 @@ class TestProleController: def test_silent_installer_does_not_generate_db_password_when_openbao_ref_present( self, tmp_path ): - c = ProleController(tmp_path) - installer = ProleConsoleInstaller(c) + c = KnoeController(tmp_path) + installer = KnoeConsoleInstaller(c) openbao_ref = "${OPENBAO:kv/prole/test/db#password}" diff --git a/tests/installer/test_deployment.py b/tests/installer/test_deployment.py index fa77830..53e44a8 100644 --- a/tests/installer/test_deployment.py +++ b/tests/installer/test_deployment.py @@ -1,6 +1,7 @@ -"""Tests for prole/deployment.py – ProleDeployment class.""" +"""Tests for prole/deployment.py – KnoeDeployment class.""" from __future__ import annotations + import configparser import os import subprocess @@ -8,10 +9,8 @@ from pathlib import Path from unittest import mock from unittest.mock import MagicMock, patch, call -import pytest - from prole.deployment import ( - ProleDeployment, + KnoeDeployment, _CFG_KEY_MAP, KUBECONFIG_FILENAME, FETCH_PLAYBOOK, @@ -59,7 +58,7 @@ def _make_deployment(tmp_path, sections=None): cfg_path = _make_cfg_file(tmp_path, sections) ctrl = _make_controller(tmp_path) with mock.patch.dict(os.environ, {"PROLE_CONF": str(tmp_path / "conf")}): - dep = ProleDeployment(ctrl, tmp_path) + dep = KnoeDeployment(ctrl, tmp_path) return dep, ctrl @@ -68,7 +67,7 @@ def _make_deployment(tmp_path, sections=None): # --------------------------------------------------------------------------- -class TestProleDeploymentInit: +class TestKnoeDeploymentInit: def test_init_loads_cfg(self, tmp_path): dep, _ = _make_deployment(tmp_path) assert dep.prole_cfg_data["Global"]["NAMESPACE"] == "test-ns" @@ -86,14 +85,14 @@ class TestProleDeploymentInit: cfg.write(f) ctrl = _make_controller(tmp_path) with mock.patch.dict(os.environ, {"PROLE_CONF": str(conf_dir)}): - dep = ProleDeployment(ctrl, tmp_path) + dep = KnoeDeployment(ctrl, tmp_path) assert dep.prole_cfg_data["Global"]["NAMESPACE"] == "knoe-db" def test_init_no_cfg_file(self, tmp_path): """When no prole.cfg exists, it still initialises.""" ctrl = _make_controller(tmp_path) with mock.patch.dict(os.environ, {"PROLE_CONF": ""}, clear=False): - dep = ProleDeployment(ctrl, tmp_path) + dep = KnoeDeployment(ctrl, tmp_path) assert dep.prole_cfg_data["Global"]["NAMESPACE"] == "knoe-db" def test_load_cfg_from_directory(self, tmp_path): @@ -101,7 +100,7 @@ class TestProleDeploymentInit: _make_cfg_file(tmp_path) ctrl = _make_controller(tmp_path) with mock.patch.dict(os.environ, {"PROLE_CONF": str(tmp_path / "conf")}): - dep = ProleDeployment(ctrl, tmp_path) + dep = KnoeDeployment(ctrl, tmp_path) assert dep.cfg_path == tmp_path / "conf" / "prole.cfg" @@ -284,7 +283,7 @@ class TestApply: dep.prole_cfg_data["Deployment"] = {"OPENTOFU_PIPELINE_DIR": str(pipeline)} assert dep.apply() is False - @patch.object(ProleDeployment, "_run_post_apply_scripts", return_value=True) + @patch.object(KnoeDeployment, "_run_post_apply_scripts", return_value=True) @patch("subprocess.run") def test_apply_success(self, mock_run, mock_post, tmp_path): pipeline = tmp_path / "pipeline" @@ -416,7 +415,7 @@ class TestRunSupabaseDeploy: class TestDuplicateK3dToK3s: - @patch.object(ProleDeployment, "_fetch_kubeconfig", return_value=None) + @patch.object(KnoeDeployment, "_fetch_kubeconfig", return_value=None) @patch( "prole.deployment._detect_ansible_topology", return_value={"k3s_server_url": "https://example.com:6443"}, @@ -431,7 +430,7 @@ class TestDuplicateK3dToK3s: assert dep.duplicate_k3d_to_k3s() is False @patch("prole.deployment._sync_opentofu_pipeline", side_effect=Exception("boom")) - @patch.object(ProleDeployment, "_fetch_kubeconfig") + @patch.object(KnoeDeployment, "_fetch_kubeconfig") @patch( "prole.deployment._detect_ansible_topology", return_value={ @@ -447,7 +446,7 @@ class TestDuplicateK3dToK3s: assert dep.duplicate_k3d_to_k3s() is False @patch("prole.deployment._sync_opentofu_pipeline") - @patch.object(ProleDeployment, "_fetch_kubeconfig") + @patch.object(KnoeDeployment, "_fetch_kubeconfig") @patch( "prole.deployment._detect_ansible_topology", return_value={ diff --git a/tests/installer/test_env_helpers.py b/tests/installer/test_env_helpers.py index 640f467..4b9de92 100644 --- a/tests/installer/test_env_helpers.py +++ b/tests/installer/test_env_helpers.py @@ -9,8 +9,33 @@ from pathlib import Path from unittest import mock import pytest - - +# --------------------------------------------------------------------------- +# config.py helpers +# --------------------------------------------------------------------------- +from knoe.config import ( + _parse_bool as cfg_parse_bool, + _expand_path as cfg_expand_path, + _expand_cfg_value as cfg_expand_cfg_value, + _collect_cfg_vars as cfg_collect_cfg_vars, + _filter_cfg_values_for_persistence, + _normalize_cfg_value_for_persistence, + _extract_yaml_scalar_from_text as cfg_extract_yaml, + _extract_inline_vault_block as cfg_extract_vault, + _load_properties, + _write_k3s_kubeconfig, + _merge_kubeconfig, + normalize_version, + get_resource_path as cfg_get_resource_path, + get_docker_build_platform_args as cfg_get_docker_args, + _is_prole_secret as cfg_is_prole_secret, + _is_openbao_ref as cfg_is_openbao_ref, + _encrypt_cfg_secret, + _resolve_secret_value, + _update_prole_cfg_value, + get_properties, + get_config_value, + setup_logging, +) # --------------------------------------------------------------------------- # env.py helpers # --------------------------------------------------------------------------- @@ -52,36 +77,6 @@ from knoe.core.env import ( _collect_cfg_vars as env_collect_cfg_vars, get_resource_path, is_apple_silicon, - get_docker_build_platform_args, -) - -# --------------------------------------------------------------------------- -# config.py helpers -# --------------------------------------------------------------------------- -from knoe.config import ( - _parse_bool as cfg_parse_bool, - _expand_path as cfg_expand_path, - _expand_cfg_value as cfg_expand_cfg_value, - _collect_cfg_vars as cfg_collect_cfg_vars, - _filter_cfg_values_for_persistence, - _normalize_cfg_value_for_persistence, - _extract_yaml_scalar_from_text as cfg_extract_yaml, - _extract_inline_vault_block as cfg_extract_vault, - _load_properties, - _write_k3s_kubeconfig, - _merge_kubeconfig, - normalize_version, - get_resource_path as cfg_get_resource_path, - is_apple_silicon as cfg_is_apple_silicon, - get_docker_build_platform_args as cfg_get_docker_args, - _is_prole_secret as cfg_is_prole_secret, - _is_openbao_ref as cfg_is_openbao_ref, - _encrypt_cfg_secret, - _resolve_secret_value, - _update_prole_cfg_value, - get_properties, - get_config_value, - setup_logging, ) @@ -894,7 +889,7 @@ class TestProperties: # ===== _update_prole_cfg_value ===== -class TestUpdateProleCfgValue: +class TestUpdateKnoeCfgValue: def test_creates_section(self, tmp_path): cfg_file = tmp_path / "conf" / "prole.cfg" cfg_file.parent.mkdir(parents=True) @@ -1008,7 +1003,7 @@ class TestPlatformHelpers: # ===== _render_prole_cfg ===== -class TestRenderProleCfg: +class TestRenderKnoeCfg: def test_minimal(self): from knoe.core.env import _render_prole_cfg diff --git a/tests/installer/test_environment_screen_expands_shell_vars.py b/tests/installer/test_environment_screen_expands_shell_vars.py index 26fcfe5..939e61d 100644 --- a/tests/installer/test_environment_screen_expands_shell_vars.py +++ b/tests/installer/test_environment_screen_expands_shell_vars.py @@ -3,8 +3,6 @@ from __future__ import annotations import os from pathlib import Path -import pytest - def test_environment_screen_expands_shell_vars_for_filesystem(monkeypatch, tmp_path: Path): """Regression: `$HOME`/`$PROLE_HOME` must be expanded before mkdir/copy. diff --git a/tests/installer/test_gitops_choice_screen.py b/tests/installer/test_gitops_choice_screen.py index 3e7ecd9..8956d24 100644 --- a/tests/installer/test_gitops_choice_screen.py +++ b/tests/installer/test_gitops_choice_screen.py @@ -1,9 +1,9 @@ """Tests for the GitOps provider choice screen (_render_gitops_choice_page).""" from __future__ import annotations + +import sys import tkinter as tk from pathlib import Path -import sys -import pytest PROJECT_ROOT = Path(__file__).resolve().parents[2] if str(PROJECT_ROOT) not in sys.path: diff --git a/tests/installer/test_milestones.py b/tests/installer/test_milestones.py index 676d26b..998f890 100644 --- a/tests/installer/test_milestones.py +++ b/tests/installer/test_milestones.py @@ -11,7 +11,7 @@ from knoe.core.milestones import ( DeploymentMilestone, ) from knoe.state import InstallerState -from knoe.core.controller import ProleController +from knoe.core.controller import KnoeController class TestMilestones(unittest.TestCase): @@ -19,7 +19,7 @@ class TestMilestones(unittest.TestCase): self.state = InstallerState() self.state.inputs["dependencies.auto_install_missing"] = "True" self.state.inputs["network_scan.run"] = "True" - self.state.controller = MagicMock(spec=ProleController) + self.state.controller = MagicMock(spec=KnoeController) @patch("knoe.config.get_dep_info") @patch("subprocess.run") diff --git a/tests/installer/test_milestones_extended.py b/tests/installer/test_milestones_extended.py index 6b3ed7c..ccf350d 100644 --- a/tests/installer/test_milestones_extended.py +++ b/tests/installer/test_milestones_extended.py @@ -24,7 +24,7 @@ from knoe.core.milestones import ( InitializationScriptsMilestone, ) from knoe.state import InstallerState -from knoe.core.controller import ProleController +from knoe.core.controller import KnoeController # --------------------------------------------------------------------------- @@ -34,7 +34,7 @@ from knoe.core.controller import ProleController def _make_state(**inputs) -> InstallerState: state = InstallerState() state.inputs.update(inputs) - state.controller = MagicMock(spec=ProleController) + state.controller = MagicMock(spec=KnoeController) state.controller.project_root = Path("/tmp/fake-prole") state.controller.get_knoe_db_version.return_value = "18-001" state.controller.run_script.return_value = 0 diff --git a/tests/installer/test_monitor.py b/tests/installer/test_monitor.py index 5a77546..13ddf41 100644 --- a/tests/installer/test_monitor.py +++ b/tests/installer/test_monitor.py @@ -10,7 +10,6 @@ from __future__ import annotations import subprocess from pathlib import Path from unittest.mock import MagicMock, patch, call -import pytest from knoe.core.monitor import ( _parse_pf_cfg, diff --git a/tests/installer/test_registry_k3s_selection.py b/tests/installer/test_registry_k3s_selection.py index 8fdf96f..a28acfe 100644 --- a/tests/installer/test_registry_k3s_selection.py +++ b/tests/installer/test_registry_k3s_selection.py @@ -1,14 +1,9 @@ from __future__ import annotations - from pathlib import Path - -import pytest - - -from knoe.ui.screens.docker import DockerScreenMixin import knoe.ui.screens.docker as docker_mod +from knoe.ui.screens.docker import DockerScreenMixin class _DummyDockerScreen(DockerScreenMixin): diff --git a/tests/test_database_options.py b/tests/test_database_options.py index ac7b485..7e96867 100644 --- a/tests/test_database_options.py +++ b/tests/test_database_options.py @@ -1,9 +1,11 @@ import sys -from unittest.mock import patch, MagicMock -from contextlib import ExitStack -import pytest -from pathlib import Path import tkinter as tk +from contextlib import ExitStack +from pathlib import Path +from unittest.mock import patch, MagicMock + +import pytest + class MockVar: def __init__(self, value=None): @@ -25,8 +27,7 @@ sys.modules["PIL"] = MagicMock() sys.modules["PIL.Image"] = MagicMock() sys.modules["PIL.ImageTk"] = MagicMock() -import install -from install import ProleInstaller +from install import KnoeInstaller def _generated_dockerfile_path(installer, tmp_path): @@ -43,8 +44,8 @@ def mock_installer(tmp_path): patch("knoe.ui.screens.database_options.PROJECT_ROOT", tmp_path), patch("knoe.ui.screens.database_options.resolve_prole_home", return_value=tmp_path), patch("knoe.ui.screens.database_options.get_resource_path", side_effect=lambda name: tmp_path / name), - patch("knoe.core.controller.ProleController.run_script"), - patch.object(ProleInstaller, "_load_database_versions"), + patch("knoe.core.controller.KnoeController.run_script"), + patch.object(KnoeInstaller, "_load_database_versions"), patch("knoe.ui.screens.database_options.tk.BooleanVar", side_effect=lambda value=None: MockVar(value)), patch("knoe.ui.screens.database_options.tk.StringVar", side_effect=lambda value=None: MockVar(value)), patch("knoe.ui.screens.database_options.tk.ttk.Treeview"), @@ -67,7 +68,7 @@ def mock_installer(tmp_path): (tmp_path / "conf" / "postgresql").mkdir(parents=True) (tmp_path / "etc").mkdir() - app = ProleInstaller(root) + app = KnoeInstaller(root) app.bg_canvas = MagicMock() app.root = root yield app diff --git a/tests/test_install_logic.py b/tests/test_install_logic.py index 7802312..7db13a5 100644 --- a/tests/test_install_logic.py +++ b/tests/test_install_logic.py @@ -17,17 +17,16 @@ import pytest from pathlib import Path # Now import install.py after mocking -import install -from install import ProleInstaller +from install import KnoeInstaller @pytest.fixture def installer(tmp_path): # Mocking os.environ to avoid messing with real env with patch.dict(os.environ, {}, clear=True): - # We need to mock ProleInstaller.__init__ because it creates GUI elements - with patch.object(ProleInstaller, "__init__", return_value=None): - ins = ProleInstaller() + # We need to mock KnoeInstaller.__init__ because it creates GUI elements + with patch.object(KnoeInstaller, "__init__", return_value=None): + ins = KnoeInstaller() # Core helpers expect `inputs` to exist; bypassing `__init__` means # we must seed minimal state for method calls. ins.inputs = {} diff --git a/tests/test_k3s_common_core_namespace_and_repair.py b/tests/test_k3s_common_core_namespace_and_repair.py index 2bbc7a7..f39a614 100644 --- a/tests/test_k3s_common_core_namespace_and_repair.py +++ b/tests/test_k3s_common_core_namespace_and_repair.py @@ -2,8 +2,8 @@ from __future__ import annotations from pathlib import Path -from knoe.core.actions import ProleConsoleInstaller -from knoe.core.controller import ProleController +from knoe.core.actions import KnoeConsoleInstaller +from knoe.core.controller import KnoeController def test_silent_installer_service_namespace_defaults_to_knoe_system_for_k3s(tmp_path, monkeypatch): @@ -11,8 +11,8 @@ def test_silent_installer_service_namespace_defaults_to_knoe_system_for_k3s(tmp_ monkeypatch.delenv("SERVICE_NAMESPACE", raising=False) - c = ProleController(tmp_path) - installer = ProleConsoleInstaller(c) + c = KnoeController(tmp_path) + installer = KnoeConsoleInstaller(c) installer.inputs["init_cluster.cluster_env"] = "service" installer.prole_cfg_data = {"Global": {}} diff --git a/tests/test_navigation.py b/tests/test_navigation.py index 295405a..90a7c93 100644 --- a/tests/test_navigation.py +++ b/tests/test_navigation.py @@ -1,7 +1,8 @@ import sys -from unittest.mock import patch, MagicMock -import pytest from pathlib import Path +from unittest.mock import patch, MagicMock + +import pytest # Mock tkinter and other GUI/macOS specific imports sys.modules["tkinter"] = MagicMock() @@ -15,8 +16,7 @@ sys.modules["PIL"] = MagicMock() sys.modules["PIL.Image"] = MagicMock() sys.modules["PIL.ImageTk"] = MagicMock() -import install -from install import ProleInstaller +from install import KnoeInstaller @pytest.fixture @@ -35,13 +35,13 @@ def mock_installer(tmp_path): ), patch( "install.Path.mkdir" ), patch.object( - ProleInstaller, "_save_env_to_file" + KnoeInstaller, "_save_env_to_file" ), patch.object( - ProleInstaller, "_save_prole_cfg" + KnoeInstaller, "_save_prole_cfg" ), patch.object( - ProleInstaller, "_save_ansible_prole_vault" + KnoeInstaller, "_save_ansible_prole_vault" ), patch.object( - ProleInstaller, "_after_env_saved" + KnoeInstaller, "_after_env_saved" ), patch( "threading.Thread" ): @@ -52,7 +52,7 @@ def mock_installer(tmp_path): root.winfo_screenwidth.return_value = 1920 root.winfo_screenheight.return_value = 1080 - app = ProleInstaller(root, config_path=str(cfg_path)) + app = KnoeInstaller(root, config_path=str(cfg_path)) return app diff --git a/tests/test_reconcile_blocked_cluster_state.py b/tests/test_reconcile_blocked_cluster_state.py index 70df316..2db4d6b 100644 --- a/tests/test_reconcile_blocked_cluster_state.py +++ b/tests/test_reconcile_blocked_cluster_state.py @@ -5,13 +5,13 @@ from types import SimpleNamespace import pytest import knoe.core.actions as actions -from knoe.core.actions import ProleConsoleInstaller -from knoe.core.controller import ProleController +from knoe.core.actions import KnoeConsoleInstaller +from knoe.core.controller import KnoeController -def _mk_installer(tmp_path) -> ProleConsoleInstaller: - c = ProleController(tmp_path) - return ProleConsoleInstaller(c) +def _mk_installer(tmp_path) -> KnoeConsoleInstaller: + c = KnoeController(tmp_path) + return KnoeConsoleInstaller(c) def test_repair_flow_classifies_blockers_attempts_reclaim_and_avoids_blind_health_wait( diff --git a/tests/test_repair_stale_released_pv_claimref.py b/tests/test_repair_stale_released_pv_claimref.py index ba2eb06..3699224 100644 --- a/tests/test_repair_stale_released_pv_claimref.py +++ b/tests/test_repair_stale_released_pv_claimref.py @@ -6,8 +6,8 @@ import os from types import SimpleNamespace import knoe.core.actions as actions -from knoe.core.actions import ProleConsoleInstaller -from knoe.core.controller import ProleController +from knoe.core.actions import KnoeConsoleInstaller +from knoe.core.controller import KnoeController @contextmanager @@ -30,9 +30,9 @@ def _preserve_namespace_env(): os.environ[k] = v -def _mk_installer(tmp_path) -> ProleConsoleInstaller: - c = ProleController(tmp_path) - return ProleConsoleInstaller(c) +def _mk_installer(tmp_path) -> KnoeConsoleInstaller: + c = KnoeController(tmp_path) + return KnoeConsoleInstaller(c) def test_match_stale_released_pv_happy_path(tmp_path): diff --git a/tests/test_repair_update_and_supabase_flags.py b/tests/test_repair_update_and_supabase_flags.py index 9405e16..f70f5dc 100644 --- a/tests/test_repair_update_and_supabase_flags.py +++ b/tests/test_repair_update_and_supabase_flags.py @@ -67,19 +67,19 @@ NAMESPACE = test-ns def _make_installer(tmp_path: Path): - """Return a minimal ProleConsoleInstaller with mocked controller.""" - from knoe.core.actions import ProleConsoleInstaller - from knoe.core.controller import ProleController + """Return a minimal KnoeConsoleInstaller with mocked controller.""" + from knoe.core.actions import KnoeConsoleInstaller + from knoe.core.controller import KnoeController cfg_path = tmp_path / "prole.cfg" if not cfg_path.exists(): _write_cfg(cfg_path, "[Inputs]\n[Global]\n[Install]\nSTATUS = New\n") - controller = MagicMock(spec=ProleController) + controller = MagicMock(spec=KnoeController) controller.project_root = tmp_path controller.state = MagicMock() - installer = ProleConsoleInstaller.__new__(ProleConsoleInstaller) + installer = KnoeConsoleInstaller.__new__(KnoeConsoleInstaller) installer.controller = controller installer.cfg_path = cfg_path installer.inputs = {} @@ -92,14 +92,14 @@ def _make_installer(tmp_path: Path): def _defaults(installer): """Call _default_inputs() with all internal deps stubbed.""" - from knoe.core.actions import ProleConsoleInstaller + from knoe.core.actions import KnoeConsoleInstaller with ( - patch.object(ProleConsoleInstaller, "_load_inputs_from_cfg", return_value={}), - patch.object(ProleConsoleInstaller, "_initial_db_namespace", return_value="test-ns"), - patch.object(ProleConsoleInstaller, "_cnpg_cluster_name", return_value="knoe-db"), - patch.object(ProleConsoleInstaller, "_get_local_owner", return_value="root"), - patch.object(ProleConsoleInstaller, "_env_defaults", return_value={}), + patch.object(KnoeConsoleInstaller, "_load_inputs_from_cfg", return_value={}), + patch.object(KnoeConsoleInstaller, "_initial_db_namespace", return_value="test-ns"), + patch.object(KnoeConsoleInstaller, "_cnpg_cluster_name", return_value="knoe-db"), + patch.object(KnoeConsoleInstaller, "_get_local_owner", return_value="root"), + patch.object(KnoeConsoleInstaller, "_env_defaults", return_value={}), ): return installer._default_inputs() @@ -151,8 +151,8 @@ def test_main_update_flag_dispatches_to_run_update(tmp_path, monkeypatch): monkeypatch.setattr(sys, "argv", ["install.py", "--update", "-c", str(cfg_path)]) monkeypatch.setattr(screens_mod, "_ensure_ansible_vault_credentials", lambda **_: None) - monkeypatch.setattr(screens_mod, "ProleController", MagicMock(return_value=MagicMock())) - monkeypatch.setattr(screens_mod, "ProleConsoleInstaller", MagicMock(return_value=installer)) + monkeypatch.setattr(screens_mod, "KnoeController", MagicMock(return_value=MagicMock())) + monkeypatch.setattr(screens_mod, "KnoeConsoleInstaller", MagicMock(return_value=installer)) with pytest.raises(SystemExit) as ex: screens_mod.main() @@ -198,7 +198,7 @@ def test_main_rejects_delete_db_without_reset(tmp_path, monkeypatch): def test_main_delete_db_is_forwarded_to_console_installer(tmp_path, monkeypatch): - """install.py --delete-db must be forwarded to ProleConsoleInstaller in silent mode.""" + """install.py --delete-db must be forwarded to KnoeConsoleInstaller in silent mode.""" import knoe.ui.screens as screens_mod cfg_path = tmp_path / "prole.cfg" @@ -217,8 +217,8 @@ def test_main_delete_db_is_forwarded_to_console_installer(tmp_path, monkeypatch) ["install.py", "--silent", "--reset", "--delete-db", "-c", str(cfg_path)], ) monkeypatch.setattr(screens_mod, "_ensure_ansible_vault_credentials", lambda **_: None) - monkeypatch.setattr(screens_mod, "ProleController", MagicMock(return_value=MagicMock())) - monkeypatch.setattr(screens_mod, "ProleConsoleInstaller", installer_factory) + monkeypatch.setattr(screens_mod, "KnoeController", MagicMock(return_value=MagicMock())) + monkeypatch.setattr(screens_mod, "KnoeConsoleInstaller", installer_factory) with pytest.raises(SystemExit) as ex: screens_mod.main() @@ -233,7 +233,7 @@ def test_main_delete_db_is_forwarded_to_console_installer(tmp_path, monkeypatch) def test_repair_flag_does_not_trigger_silent_install(tmp_path): """--repair must dispatch to run_repair(), NOT run() (full install).""" - from knoe.core.actions import ProleConsoleInstaller + from knoe.core.actions import KnoeConsoleInstaller installer = _make_installer(tmp_path) @@ -241,9 +241,9 @@ def test_repair_flag_does_not_trigger_silent_install(tmp_path): run_repair_called = [] with ( - patch.object(ProleConsoleInstaller, "run", side_effect=lambda: run_called.append(1) or 0), + patch.object(KnoeConsoleInstaller, "run", side_effect=lambda: run_called.append(1) or 0), patch.object( - ProleConsoleInstaller, + KnoeConsoleInstaller, "run_repair", side_effect=lambda: run_repair_called.append(1) or 0, ), @@ -256,38 +256,38 @@ def test_repair_flag_does_not_trigger_silent_install(tmp_path): def test_update_calls_repair_first(tmp_path): """run_update() must invoke run_repair() before applying config changes.""" - from knoe.core.actions import ProleConsoleInstaller + from knoe.core.actions import KnoeConsoleInstaller installer = _make_installer(tmp_path) call_order: list[str] = [] with ( patch.object( - ProleConsoleInstaller, + KnoeConsoleInstaller, "run_repair", side_effect=lambda: call_order.append("repair") or 0, ), - patch.object(ProleConsoleInstaller, "_load_inputs_from_cfg", return_value={}), - patch.object(ProleConsoleInstaller, "_default_inputs", return_value={}), - patch.object(ProleConsoleInstaller, "_service_namespace", return_value="test-ns"), - patch.object(ProleConsoleInstaller, "_script_env_for_namespace", return_value={}), + patch.object(KnoeConsoleInstaller, "_load_inputs_from_cfg", return_value={}), + patch.object(KnoeConsoleInstaller, "_default_inputs", return_value={}), + patch.object(KnoeConsoleInstaller, "_service_namespace", return_value="test-ns"), + patch.object(KnoeConsoleInstaller, "_script_env_for_namespace", return_value={}), patch.object( - ProleConsoleInstaller, + KnoeConsoleInstaller, "_reconcile_supabase", side_effect=lambda env: call_order.append("reconcile"), ), patch.object( - ProleConsoleInstaller, + KnoeConsoleInstaller, "_step_db_build", side_effect=lambda: call_order.append("db_build"), ), patch.object( - ProleConsoleInstaller, + KnoeConsoleInstaller, "_step_cnpg_deploy", side_effect=lambda: call_order.append("cnpg_deploy"), ), - patch.object(ProleConsoleInstaller, "_write_cfg", return_value=None), - patch.object(ProleConsoleInstaller, "_close_log_file", return_value=None), + patch.object(KnoeConsoleInstaller, "_write_cfg", return_value=None), + patch.object(KnoeConsoleInstaller, "_close_log_file", return_value=None), ): installer.run_update() @@ -298,15 +298,15 @@ def test_update_calls_repair_first(tmp_path): def test_update_forces_cnpg_rollout_only_for_deploy_step(tmp_path): """run_update() must force rollout for CNPG deploy, then restore configured value.""" - from knoe.core.actions import ProleConsoleInstaller + from knoe.core.actions import KnoeConsoleInstaller installer = _make_installer(tmp_path) seen_force_rollout: list[bool] = [] with ( - patch.object(ProleConsoleInstaller, "run_repair", return_value=0), + patch.object(KnoeConsoleInstaller, "run_repair", return_value=0), patch.object( - ProleConsoleInstaller, + KnoeConsoleInstaller, "_load_inputs_from_cfg", return_value={ "init_cnpg_deploy.run_deploy": "true", @@ -315,13 +315,13 @@ def test_update_forces_cnpg_rollout_only_for_deploy_step(tmp_path): "init_cluster.supabase_enabled": "false", }, ), - patch.object(ProleConsoleInstaller, "_default_inputs", return_value={}), - patch.object(ProleConsoleInstaller, "_service_namespace", return_value="test-ns"), - patch.object(ProleConsoleInstaller, "_script_env_for_namespace", return_value={}), - patch.object(ProleConsoleInstaller, "_write_cfg", return_value=None), - patch.object(ProleConsoleInstaller, "_close_log_file", return_value=None), + patch.object(KnoeConsoleInstaller, "_default_inputs", return_value={}), + patch.object(KnoeConsoleInstaller, "_service_namespace", return_value="test-ns"), + patch.object(KnoeConsoleInstaller, "_script_env_for_namespace", return_value={}), + patch.object(KnoeConsoleInstaller, "_write_cfg", return_value=None), + patch.object(KnoeConsoleInstaller, "_close_log_file", return_value=None), patch.object( - ProleConsoleInstaller, + KnoeConsoleInstaller, "_step_cnpg_deploy", side_effect=lambda: seen_force_rollout.append( installer._get_input_bool("init_cnpg_deploy.force_rollout", False) @@ -490,7 +490,7 @@ def test_render_supabase_component_flags_default_to_enabled(tmp_path, monkeypatc def test_reconcile_supabase_skips_studio_when_disabled(tmp_path): """_reconcile_supabase() must skip Studio when supabase_studio_enabled=false.""" - from knoe.core.actions import ProleConsoleInstaller + from knoe.core.actions import KnoeConsoleInstaller installer = _make_installer(tmp_path) base = _defaults(installer) @@ -502,7 +502,7 @@ def test_reconcile_supabase_skips_studio_when_disabled(tmp_path): studio_reconcile_called = [] with patch.object( - ProleConsoleInstaller, + KnoeConsoleInstaller, "_reconcile_supabase_studio", side_effect=lambda env: studio_reconcile_called.append(1), ): @@ -515,7 +515,7 @@ def test_reconcile_supabase_skips_studio_when_disabled(tmp_path): def test_reconcile_supabase_calls_studio_when_enabled(tmp_path): """_reconcile_supabase() must invoke Studio reconcile when enabled.""" - from knoe.core.actions import ProleConsoleInstaller + from knoe.core.actions import KnoeConsoleInstaller installer = _make_installer(tmp_path) base = _defaults(installer) @@ -527,7 +527,7 @@ def test_reconcile_supabase_calls_studio_when_enabled(tmp_path): studio_reconcile_called = [] with patch.object( - ProleConsoleInstaller, + KnoeConsoleInstaller, "_reconcile_supabase_studio", side_effect=lambda env: studio_reconcile_called.append(1), ): diff --git a/tests/test_service_layer_navigation.py b/tests/test_service_layer_navigation.py index 45c023c..02b897f 100644 --- a/tests/test_service_layer_navigation.py +++ b/tests/test_service_layer_navigation.py @@ -1,8 +1,9 @@ import sys from contextlib import ExitStack -from unittest.mock import patch, MagicMock -import pytest from pathlib import Path +from unittest.mock import patch, MagicMock + +import pytest # Mock tkinter and other GUI/macOS specific imports sys.modules["tkinter"] = MagicMock() @@ -15,8 +16,7 @@ sys.modules["PIL"] = MagicMock() sys.modules["PIL.Image"] = MagicMock() sys.modules["PIL.ImageTk"] = MagicMock() -import install -from install import ProleInstaller +from install import KnoeInstaller @pytest.fixture @@ -38,10 +38,10 @@ def mock_installer(tmp_path): stack.enter_context(patch("install.ttk.Style")) stack.enter_context(patch("install.Path.exists", return_value=True)) stack.enter_context(patch("install.Path.mkdir")) - stack.enter_context(patch.object(ProleInstaller, "_save_env_to_file")) - stack.enter_context(patch.object(ProleInstaller, "_save_prole_cfg")) - stack.enter_context(patch.object(ProleInstaller, "_save_ansible_prole_vault")) - stack.enter_context(patch.object(ProleInstaller, "_after_env_saved")) + stack.enter_context(patch.object(KnoeInstaller, "_save_env_to_file")) + stack.enter_context(patch.object(KnoeInstaller, "_save_prole_cfg")) + stack.enter_context(patch.object(KnoeInstaller, "_save_ansible_prole_vault")) + stack.enter_context(patch.object(KnoeInstaller, "_after_env_saved")) stack.enter_context(patch("threading.Thread")) mock_entry.return_value.get.return_value.strip.return_value = "mock_val" @@ -49,7 +49,7 @@ def mock_installer(tmp_path): root.winfo_screenwidth.return_value = 1920 root.winfo_screenheight.return_value = 1080 - app = ProleInstaller(root, config_path=str(cfg_path)) + app = KnoeInstaller(root, config_path=str(cfg_path)) try: app.bg_canvas.winfo_width.return_value = 1000 except Exception: