checkpoint: tighten CNPG k3s storage flow and policy updates

- normalize K3S/CNPG/OpenTofu env keys across installer and configs; - harden CNPG storage validation/provisioning and fail fast in init script; - update DB manifests/recovery template and port/network mapping data; - expand cluster node policy UI coverage and shell/python tests; - add k3s mode version marker files

Co-authored-by: Junie <junie@jetbrains.com>
This commit is contained in:
chrisfu 2026-03-23 12:39:03 -07:00
parent 9791a6663e
commit 0e806a9233
16 changed files with 613 additions and 177 deletions

View File

@ -1,5 +1,5 @@
; Prole Master Configuration File
; Generated by install.py on 2026-03-23 01:05:45
; Generated by install.py on 2026-03-23 05:47:45
; This file is used as input for Ansible deployment and k8s cluster creation.
[User]
@ -49,6 +49,8 @@ init_cluster.at_rest_encryption_enabled = true
init_cluster.cluster_env = dev
init_cluster.deployment_target = prole-dev-cluster
init_cluster.gitops_enabled = false
init_cluster.k3s_server_url = https://myrddin.prole.org:6443
init_cluster.k3s_token = ${PROLE_SECRET:v1:_SxOPUoevrdHL-EI:oQ8F1jKsGCyqYd2SQknjAb2EwrY3FIkW6e-QpoGAtIZB-kDqhWvQ3eWM5pOYMZ2lSLBvMGlIB-L6I2wI1uODnLi-R25wkZm6-n7zcs6R_ofK9F6la_TgzaDXoWFt_WyTCmmK0yFUAQ5q7ejzbGwlQD_hxvwZmybuiKNjZg==}
init_cluster.kerberos_enabled = true
init_cluster.mode = k3d
init_cluster.start_cluster = true
@ -58,14 +60,14 @@ init_cnpg_deploy.run_deploy = true
init_db_build.run_build = true
init_password.db_host_port = 5432
init_password.db_namespace = ${NAMESPACE}
init_password.db_password = ${PROLE_SECRET:v1:6--EY4qcxPxnM3dj:5giZ7B2OWXvivFwj-NQiogFb-BgrD43u}
init_password.db_password_confirm = ${PROLE_SECRET:v1:6--EY4qcxPxnM3dj:5giZ7B2OWXvivFwj-NQiogFb-BgrD43u}
init_password.db_password = ${PROLE_SECRET:v1:VtMK1Hb7NuUwl8vg:z7l5pjx2HVX9RIF_T_G6GfgDPSGu88Rq}
init_password.db_password_confirm = ${PROLE_SECRET:v1:VtMK1Hb7NuUwl8vg:z7l5pjx2HVX9RIF_T_G6GfgDPSGu88Rq}
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:lgRUp-FYr0mxvPI1:cXJTJLEzECF20A62qJIcG2RblRXrxS3VEv4JJs9GtCxa4tUK}
kerberos_config.password = ${PROLE_SECRET:v1:M_ktrIO8qCCNi00s:fdjylvLlSzeFo_CW-1YPAd3kQU_IKEzIW3JCh_pWPQBubtG_}
kerberos_config.realm = PROLE.ORG
kerberos_config.test_connection = false
kerberos_config.user = administrator
@ -77,18 +79,16 @@ ollama_config.server_port = 11434
[Global]
; Variables used by name in more than one place or assumed global scope
CLUSTER_ENV = dev
CNPG_PLACEMENT_PLAN_FILE = /Users/chrisfu/dev/prole/conf/cnpg-placement/knoe-system-knoe-db.json
CNPG_PLACEMENT_PLAN_HASH = 965d2fde4035059f
CNPG_PLACEMENT_PLAN_ID = cnpg-placement-965d2fde4035059f
DB_HOST_PORT = 5432
DB_PASSWORD = ${PROLE_SECRET:v1:wQ8-XBE35Upcd1ob:AoFolm6Hq3WsleBZ6S5q9zFp3ObF5Vis}
DEPLOYMENT_MODE = k3s
MODE = k3s
DB_PASSWORD = ${PROLE_SECRET:v1:JaHrcyC4uDthLlMB:k6aqQY7JXnSffQagK7CfJTqkzwEEYwGu}
DEPLOYMENT_MODE = k3d
DEPLOYMENT_TARGET = prole-dev-cluster
DOCKER_PRELOAD = false
KNOE_DB_USER = root
NAMESPACE = knoe-db-17-7-043-18-140
OPTIONAL_WORKLOADS_MIN_READY_SCHEDULABLE_NODES = 2
PROLE_K3S_SERVER = https://myrddin.prole.org:6443
PROLE_K3S_TOKEN = ${PROLE_SECRET:v1:cU7I0vEJL7AppE-x:Kwu5vGmJi7bKAKyKT8_fqYS-p241IM5u6DxAcI2M0HuD_sRIsIsSDtv5u3Y79OPX6rFTOEwyxPtCK44GYRQgxHNPnq2ltRe60yHXIUwBY-jBFRX864e2_c-vXUvzjkYJvruxBHhTgOa4nFavIRs_0cBPmjwP5qrVBJkpXg==}
PROLE_OPENTOFU_URL = http://127.0.0.1:8080
SERVICE_NAMESPACE = knoe-system
@ -133,10 +133,7 @@ OLLAMA_SERVER_HOST = fairyland.prole.org
OLLAMA_SERVER_PORT = 11434
[Optional Features]
AT_REST_ENCRYPTION_ENABLED = True
GITOPS_ENABLED = False
KERBEROS_ENABLED = True
SUPABASE_ENABLED = False
[GitOps]
; No configuration values captured yet for this section.
@ -145,20 +142,34 @@ SUPABASE_ENABLED = False
; No configuration values captured yet for this section.
[Initialize Cluster]
ENVIRONMENT = dev
; No configuration values captured yet for this section.
[Dev Cluster (k3d)]
CLUSTER_ENV = dev
DISPLAY_NAME = knoe-dev-cluster
KUBECTL_CONTEXT = k3d-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:t94Mb2O9TNzuCiKH:BoUY6Yj7a4NtsVmulT1MQb8aQ5w4ltS7SZDCFf9o3MfwB0YVem9QhDUin1mZGoFDVtRFOo3iF2l8PKvDSDRxajI-ENqLuFve41Y6s7tF7Wp4xH2-mNkf3xPeN2XX2iOqBhZ3UvceScSp_2iV_8bR7kaPqtNtoshr0icNxw==}
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]
LOCAL_REGISTRY = localhost:5000
LOCAL_REGISTRY_INTERNAL = k3d-prole-registry.localhost:5000
; No configuration values captured yet for this section.
[Initialization Scripts]
STATUS = Attempted
; No configuration values captured yet for this section.
[Deployment]
MODE = k3d

View File

@ -3,10 +3,9 @@
argocd: local=8081 remote=80 ns=argocd svc=argocd-server address=0.0.0.0
garage: local=3900 remote=3900 ns=knoe-system svc=garage address=0.0.0.0
openbao: local=8200 remote=8200 ns=knoe-system svc=openbao address=127.0.0.1
openbao: local=8200 remote=8200 ns=knoe-system svc=openbao address=0.0.0.0
opentofu: local=8080 remote=8080 ns=knoe-system svc=opentofu address=0.0.0.0
dashboard: local=8443 remote=443 ns=kubernetes-dashboard svc=kubernetes-dashboard-kong-proxy address=127.0.0.1
postgres: local=5432 remote=5432 ns=knoe-db-17-7-043-18-140 svc=knoe-db-rw address=0.0.0.0
postgres: local=5432 remote=5432 ns=knoe-db svc=knoe-db-rw address=0.0.0.0
prometheus: local=9090 remote=9090 ns=monitoring svc=kps-kube-prometheus-stack-prometheus address=127.0.0.1
grafana: local=3000 remote=80 ns=monitoring svc=kps-grafana address=0.0.0.0
certmgr: local=9443 remote=9443 ns=cert-manager svc=cert-manager-webhook address=127.0.0.1

View File

@ -1,5 +1,5 @@
; Prole Master Configuration File
; Generated by install.py on 2026-03-22 20:46:43
; Generated by install.py on 2026-03-23 11:51:58
; This file is used as input for Ansible deployment and k8s cluster creation.
[User]
@ -29,28 +29,28 @@ dependencies.ansible.install = true
dependencies.auto_install_missing = true
dependencies.brew.install = true
dependencies.docker.install = true
dependencies.k3d.install = false
dependencies.k3d.install = true
dependencies.kubectl.install = true
dependencies.kubectx.install = true
dependencies.opentofu.install = true
dependencies.python.install = true
dependencies.verify_all = false
disk_selection.disk_type = local
disk_selection.local_path = $HOME/dev/prole/prole-tools-app/dist
disk_selection.local_path = /Users/chrisfu/dev/prole/prole-tools-app/dist
disk_selection.removable_mount =
env_setup.NAMESPACE = ${NAMESPACE}
env_setup.PROLE_CONF = $HOME/dev/prole/conf
env_setup.PROLE_DATA = $HOME/dev/prole/data
env_setup.PROLE_HOME = $HOME/dev/prole
env_setup.PROLE_LOGS = $HOME/dev/prole/logs
env_setup.PROLE_SERVICE = $HOME/dev/prole/etc
env_setup.PROLE_CONF = /Users/chrisfu/dev/prole/conf
env_setup.PROLE_DATA = /Users/chrisfu/dev/prole/data
env_setup.PROLE_HOME = /Users/chrisfu/dev/prole
env_setup.PROLE_LOGS = /Users/chrisfu/dev/prole/logs
env_setup.PROLE_SERVICE = /Users/chrisfu/dev/prole/etc
init_cluster.argocd_enabled = false
init_cluster.at_rest_encryption_enabled = true
init_cluster.cluster_env = service
init_cluster.deployment_target = prole-service-cluster
init_cluster.gitops_enabled = false
init_cluster.k3s_server_url = https://myrddin.prole.org:6443
init_cluster.k3s_token = ${PROLE_SECRET:v1:_r5tIPt5VZcr0SrV:e2_sHY49gG2i-vhQZr-42M1kekln4D4ZHq6tJrDRCn8f4KOVSn59rndLt4PuHmsebPSPP4tGHZV0MaVtdBMm4RJmk9IXbqV7BjSNruK2SXi62NOXIIUWZGNcVIOFjmv7EovfOifo3nye8142zKDspTeyo5Y2KY6qGLtS2g==}
init_cluster.k3s_token = ${PROLE_SECRET:v1:WjD201Yr7MHE56cs:wackF_EDDde7KmJp2SwbMlJi5mLdoHATkoyolRkPVqvnOVVScriN55ZHOO0cec-RvMIFQzhkL_yZbKhNkN9IIfkRdnAeGPDVELtM-wEDv_YlymTsrpYL_u79VR-nX3gKTCfcAha44ZyoBzObKTShzUx8eLCJMeLKYoPEKg==}
init_cluster.kerberos_enabled = true
init_cluster.mode = k3s
init_cluster.start_cluster = true
@ -60,56 +60,51 @@ init_cnpg_deploy.run_deploy = true
init_db_build.run_build = true
init_password.db_host_port = 5432
init_password.db_namespace = ${NAMESPACE}
init_password.db_password = ${PROLE_SECRET:v1:vz4oaR5QA3zIJWDs:blYR6vqpY0e_GqGWFufDwlY4yxXWXrGF}
init_password.db_password_confirm = ${PROLE_SECRET:v1:vz4oaR5QA3zIJWDs:blYR6vqpY0e_GqGWFufDwlY4yxXWXrGF}
init_password.db_password = ${PROLE_SECRET:v1:z2-5uGBCGoUaPEJm:RPb2Hoei9ikHHjLHbXQx3Ej2AxuXDrX2}
init_password.db_password_confirm = ${PROLE_SECRET:v1:z2-5uGBCGoUaPEJm:RPb2Hoei9ikHHjLHbXQx3Ej2AxuXDrX2}
init_password.db_username = root
init_password.generate_ssh_key = true
init_scripts.run_scripts = true
kerberos_config.enabled = true
kerberos_config.init_authority = false
kerberos_config.kdc = 10.0.0.3
kerberos_config.password = ${PROLE_SECRET:v1:71ROfYyYysQDJDpX:0K4cB3p9nnMb1yi2OImqUm-pa9Zg21JYTmYHzYB_7czTKMQJ}
kerberos_config.password = ${PROLE_SECRET:v1:ctSSZbqz2VXQSk7Y:eVFtcFMoru50wpG7_1wgl1mQbsxEKfMsehQHpKcalJolggZK}
kerberos_config.realm = PROLE.ORG
kerberos_config.test_connection = false
kerberos_config.user = administrator
network_scan.run = true
ollama_config.model =
ollama_config.server_host = fairyland.prole.org
ollama_config.server_host =
ollama_config.server_port = 11434
[Global]
; Variables used by name in more than one place or assumed global scope
ARGOCD_NAMESPACE = argocd
CLUSTER_ENV = service
DB_HOST_PORT = 5432
DB_PASSWORD = ${PROLE_SECRET:v1:NmgNZUhCsk-p0_DF:VB1tKREj45eEWcnN1GAlcGV6b_Lrl9sx}
DB_PASSWORD = ${PROLE_SECRET:v1:eo18GXaCA2dv5ner:paML1Ebi5MfO_F2gnLTiAVaXdJoeRN9U}
DEPLOYMENT_MODE = k3s
MODE = k3s
DEPLOYMENT_TARGET = prole-service-cluster
DOCKER_IMPORT_DIR =
DOCKER_PRELOAD = false
KNOE_DB_USER = root
KUBECONTEXT = prole-k3s
NAMESPACE = knoe-db
OPTIONAL_WORKLOADS_MIN_READY_SCHEDULABLE_NODES = 2
KNOE_DB_USER = root
PROLE_HOME = $HOME/dev/prole
PROLE_K3S_SERVER = https://myrddin.prole.org:6443
PROLE_K3S_TOKEN = ${PROLE_SECRET:v1:_r5tIPt5VZcr0SrV:e2_sHY49gG2i-vhQZr-42M1kekln4D4ZHq6tJrDRCn8f4KOVSn59rndLt4PuHmsebPSPP4tGHZV0MaVtdBMm4RJmk9IXbqV7BjSNruK2SXi62NOXIIUWZGNcVIOFjmv7EovfOifo3nye8142zKDspTeyo5Y2KY6qGLtS2g==}
PROLE_OPENTOFU_URL = http://myrddin.prole.org:8080
REGISTRY_NAMESPACE = knoe-system
PROLE_K3S_TOKEN = ${PROLE_SECRET:v1:WjD201Yr7MHE56cs:wackF_EDDde7KmJp2SwbMlJi5mLdoHATkoyolRkPVqvnOVVScriN55ZHOO0cec-RvMIFQzhkL_yZbKhNkN9IIfkRdnAeGPDVELtM-wEDv_YlymTsrpYL_u79VR-nX3gKTCfcAha44ZyoBzObKTShzUx8eLCJMeLKYoPEKg==}
PROLE_OPENTOFU_URL = http://127.0.0.1:8080
SERVICE_NAMESPACE = knoe-system
[Welcome]
; No configuration values captured yet for this section.
[Dependencies]
STATUS = All installed
; No configuration values captured yet for this section.
[Network]
AD_DC_HOST = myrddin.prole.org
AD_DC_IP = 10.0.0.3
ANSIBLE_DOMAIN = prole.org
ANSIBLE_INFRASTRUCTURE = $HOME/dev/prole/infrastructure
ANSIBLE_INVENTORY = $HOME/dev/prole/infrastructure/inventory
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"]}
KDC_ANSIBLE_DETECTED = 10.0.0.3
@ -117,22 +112,14 @@ KDC_AUTO_DETECTED = 10.0.0.3
KERBEROS_AUTO_ENABLED = True
[Port Forwards]
PORT_FORWARD_K3S_MAPPING_1 = id=argocd;namespace=argocd;target=svc/argocd-server;address=0.0.0.0;hostPort=8081;servicePort=80;protocol=TCP;description=ArgoCD
PORT_FORWARD_K3S_MAPPING_2 = id=garage;namespace=knoe-system;target=svc/garage;address=0.0.0.0;hostPort=3900;servicePort=3900;protocol=TCP;description=Garage S3
PORT_FORWARD_K3S_MAPPING_3 = id=openbao;namespace=knoe-system;target=svc/openbao;address=0.0.0.0;hostPort=8200;servicePort=8200;protocol=TCP;description=OpenBao
PORT_FORWARD_K3S_MAPPING_4 = id=opentofu;namespace=knoe-system;target=svc/opentofu;address=0.0.0.0;hostPort=8080;servicePort=8080;protocol=TCP;description=OpenTofu
PORT_FORWARD_K3S_MAPPING_5 = id=dashboard;namespace=kubernetes-dashboard;target=svc/kubernetes-dashboard-kong-proxy;address=127.0.0.1;hostPort=8443;servicePort=443;protocol=TCP;description=Kubernetes Dashboard
PORT_FORWARD_K3S_MAPPING_6 = id=postgres;namespace=${NAMESPACE};target=svc/knoe-db-rw;address=0.0.0.0;hostPort=5432;servicePort=5432;protocol=TCP;description=PostgreSQL (primary)
PORT_FORWARD_K3S_MAPPING_7 = id=prometheus;namespace=monitoring;target=svc/kps-kube-prometheus-stack-prometheus;address=127.0.0.1;hostPort=9090;servicePort=9090;protocol=TCP;description=Prometheus UI
PORT_FORWARD_K3S_MAPPING_8 = id=grafana;namespace=monitoring;target=svc/kps-grafana;address=0.0.0.0;hostPort=3000;servicePort=80;protocol=TCP;description=Grafana UI
; No configuration values captured yet for this section.
[System Environment]
NAMESPACE = knoe-db
PROLE_CONF = $HOME/dev/prole/conf
PROLE_DATA = $HOME/dev/prole/data
PROLE_HOME = $HOME/dev/prole
PROLE_LOGS = $HOME/dev/prole/logs
PROLE_SERVICE = $HOME/dev/prole/etc
PROLE_CONF = /Users/chrisfu/dev/prole/conf
PROLE_DATA = /Users/chrisfu/dev/prole/data
PROLE_HOME = /Users/chrisfu/dev/prole
PROLE_LOGS = /Users/chrisfu/dev/prole/logs
PROLE_SERVICE = /Users/chrisfu/dev/prole/etc
[Monitoring]
; No configuration values captured yet for this section.
@ -141,43 +128,55 @@ PROLE_SERVICE = $HOME/dev/prole/etc
; No configuration values captured yet for this section.
[Ollama]
OLLAMA_HOST = http://fairyland.prole.org:11434
OLLAMA_SERVERS = fairyland.prole.org:11434,morgoth.prole.org:11434
OLLAMA_SERVER_HOST = fairyland.prole.org
OLLAMA_SERVER_PORT = 11434
; No configuration values captured yet for this section.
[Optional Features]
AT_REST_ENCRYPTION_ENABLED = true
GITOPS_ENABLED = false
KERBEROS_ENABLED = true
SUPABASE_ENABLED = false
AT_REST_ENCRYPTION_ENABLED = True
GITOPS_ENABLED = False
KERBEROS_ENABLED = True
SUPABASE_ENABLED = False
[GitOps]
; No configuration values captured yet for this section.
[Database Creation]
DB_NAME = knoe-db-0
DB_USER = root
; No configuration values captured yet for this section.
[Initialize Cluster]
; No configuration values captured yet for this section.
ENVIRONMENT = service
K3S_SERVER_URL = https://myrddin.prole.org:6443
K3S_TOKEN = ${PROLE_SECRET:v1:WjD201Yr7MHE56cs:wackF_EDDde7KmJp2SwbMlJi5mLdoHATkoyolRkPVqvnOVVScriN55ZHOO0cec-RvMIFQzhkL_yZbKhNkN9IIfkRdnAeGPDVELtM-wEDv_YlymTsrpYL_u79VR-nX3gKTCfcAha44ZyoBzObKTShzUx8eLCJMeLKYoPEKg==}
[Dev Cluster (k3d)]
CLUSTER_ENV = dev
DISPLAY_NAME = knoe-dev-cluster
KUBECTL_CONTEXT = prole-k3s
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:_r5tIPt5VZcr0SrV:e2_sHY49gG2i-vhQZr-42M1kekln4D4ZHq6tJrDRCn8f4KOVSn59rndLt4PuHmsebPSPP4tGHZV0MaVtdBMm4RJmk9IXbqV7BjSNruK2SXi62NOXIIUWZGNcVIOFjmv7EovfOifo3nye8142zKDspTeyo5Y2KY6qGLtS2g==}
K3S_TOKEN = ${PROLE_SECRET:v1:WjD201Yr7MHE56cs:wackF_EDDde7KmJp2SwbMlJi5mLdoHATkoyolRkPVqvnOVVScriN55ZHOO0cec-RvMIFQzhkL_yZbKhNkN9IIfkRdnAeGPDVELtM-wEDv_YlymTsrpYL_u79VR-nX3gKTCfcAha44ZyoBzObKTShzUx8eLCJMeLKYoPEKg==}
MODE = k3s
PIPELINE_URL = http://myrddin.prole.org:8080
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.
[Initialization Scripts]
; No configuration values captured yet for this section.
STATUS = Attempted
[Deployment]
MODE = k3s
TARGET = prole-service-cluster
[Install]
STATUS = Failed
; No configuration values captured yet for this section.

View File

@ -4,7 +4,7 @@ metadata:
name: knoe-db
spec:
instances: 3
imageName: k3d-prole-registry:5000/knoe-db:18-140
imageName: registry.test-system.svc.cluster.local:5000/knoe-db:18-140
postgresUID: 100
postgresGID: 101
maxSyncReplicas: 1

View File

@ -106,10 +106,12 @@ BARMAN_PLUGIN_FALLBACK_VERSION=${BARMAN_PLUGIN_FALLBACK_VERSION:-0.9.0}
CERT_MANAGER_MANIFEST_URL=${CERT_MANAGER_MANIFEST_URL:-}
CERT_MANAGER_FALLBACK_VERSION=${CERT_MANAGER_FALLBACK_VERSION:-1.19.3}
# Protected storage requirements (k3s only)
PROLE_CNPG_STORAGE_CLASS=${PROLE_CNPG_STORAGE_CLASS:-synology-iscsi}
PROLE_PROTECTED_DATA_PATH=${PROLE_PROTECTED_DATA_PATH:-/synology/d001/data}
PROLE_PROTECTED_WAL_PATH=${PROLE_PROTECTED_WAL_PATH:-/synology/d001/wal}
# CNPG storage requirements (k3s only)
CNPG_STORAGE_CLASS=${CNPG_STORAGE_CLASS:-synology-iscsi}
SYNOLOGY_ROOTS=${SYNOLOGY_ROOTS:-/synology/d001,/synology/d002,/synology/d003,/synology/d004}
# Legacy compatibility vars kept for callers that still export them.
PROLE_PROTECTED_DATA_PATH=${PROLE_PROTECTED_DATA_PATH:-}
PROLE_PROTECTED_WAL_PATH=${PROLE_PROTECTED_WAL_PATH:-}
if [[ "$ACTION" != "deploy" && "$ACTION" != "rollout" && "$ACTION" != "force-rollout" ]]; then
if [[ -n "${2:-}" ]]; then
@ -169,18 +171,17 @@ _require_tool() {
command -v "$t" >/dev/null 2>&1 || { echo "ERROR: Missing required tool: $t" >&2; return 1; }
}
_require_prole_protected_mount() {
_require_non_root_mount() {
local path="$1" label="$2"
local findmnt_bin="${PROLE_FINDMNT_BIN:-findmnt}"
if [[ ! -d "$path" ]]; then
echo "ERROR: Protected ${label} path missing: ${path}" >&2
echo "Refusing CNPG deployment: Prole protected storage must be mounted at ${PROLE_PROTECTED_DATA_PATH} and ${PROLE_PROTECTED_WAL_PATH}." >&2
echo "ERROR: ${label} path missing: ${path}" >&2
return 1
fi
_require_tool "$findmnt_bin" || {
echo "ERROR: '${findmnt_bin}' is required to validate protected storage mounts (k3s mode)." >&2
echo "ERROR: '${findmnt_bin}' is required to validate storage mounts (k3s mode)." >&2
return 1
}
@ -196,21 +197,104 @@ _require_prole_protected_mount() {
fi
if [[ "$path_target" == "/" || "$path_src" == "$root_src" ]]; then
echo "ERROR: Protected ${label} path '${path}' is backed by the root filesystem (target='${path_target}', source='${path_src}')." >&2
echo "ERROR: ${label} path '${path}' is backed by the root filesystem (target='${path_target}', source='${path_src}')." >&2
echo "Refusing CNPG deployment to prevent root/SD-backed storage from being used." >&2
return 1
fi
return 0
}
_validate_cnpg_host_path() {
local path="$1" role="$2"
shift 2
local roots=("$@")
if [[ -z "$path" ]]; then
echo "ERROR: Missing computed CNPG ${role} host path for k3s storage validation." >&2
return 1
fi
if ! _require_non_root_mount "$path" "CNPG ${role}"; then
return 1
fi
local expected_suffix="/${NAMESPACE}/${CNPG_CLUSTER_NAME}/${role}"
if [[ "$path" != *"$expected_suffix" ]]; then
echo "ERROR: Computed CNPG ${role} host path '${path}' does not match expected suffix '${expected_suffix}'." >&2
return 1
fi
local root
for root in "${roots[@]}"; do
if [[ "$path" == "$root" || "$path" == "$root"/* ]]; then
return 0
fi
done
echo "ERROR: Computed CNPG ${role} host path '${path}' is not under configured Synology roots (${roots[*]})." >&2
return 1
}
ensure_prole_protected_storage() {
if [[ "${PROLE_MODE:-}" != "k3s" ]]; then
return 0
fi
echo "Validating protected Prole storage mounts (k3s mode) ..."
_require_prole_protected_mount "$PROLE_PROTECTED_DATA_PATH" "data" || return 1
_require_prole_protected_mount "$PROLE_PROTECTED_WAL_PATH" "WAL" || return 1
echo "Validating Synology storage roots and CNPG host paths (k3s mode) ..."
local target_node="${CNPG_STAGE1_NODE:-}"
local local_host="" local_short="" local_fqdn=""
local_host=$(hostname 2>/dev/null || true)
local_short=$(hostname -s 2>/dev/null || true)
local_fqdn=$(hostname -f 2>/dev/null || true)
if [[ -z "$local_short" && -n "$local_host" ]]; then
local_short="$local_host"
fi
if [[ -z "$local_fqdn" && -n "$local_host" ]]; then
local_fqdn="$local_host"
fi
local run_local_checks=0
if [[ -z "$target_node" ]]; then
run_local_checks=1
elif [[ "$target_node" == "$local_fqdn" || "$target_node" == "$local_short" || "$target_node" == "$local_host" ]]; then
run_local_checks=1
elif [[ -n "$local_fqdn" && "$local_fqdn" == "$target_node".* ]]; then
run_local_checks=1
elif [[ -n "$local_short" && "$target_node" == "$local_short".* ]]; then
run_local_checks=1
fi
if (( run_local_checks == 0 )); then
echo "INFO: Skipping local Synology storage preflight because CNPG target node '${target_node}' is remote from this host ('${local_fqdn:-${local_short:-unknown}}')." >&2
echo "INFO: Remote node storage validation is not available yet; continuing with configured CNPG storage paths." >&2
return 0
fi
local root roots=()
for root in ${SYNOLOGY_ROOTS//,/ }; do
[[ -n "$root" ]] && roots+=("$root")
done
if (( ${#roots[@]} == 0 )); then
echo "ERROR: SYNOLOGY_ROOTS is empty; expected one or more Synology storage roots." >&2
return 1
fi
for root in "${roots[@]}"; do
_require_non_root_mount "$root" "Synology storage root" || return 1
done
local data_host_path="${CNPG_DATA_HOST_PATH:-}"
local wal_host_path="${CNPG_WAL_HOST_PATH:-}"
if [[ -n "$data_host_path" || -n "$wal_host_path" ]]; then
_validate_cnpg_host_path "$data_host_path" "data" "${roots[@]}" || return 1
_validate_cnpg_host_path "$wal_host_path" "wal" "${roots[@]}" || return 1
else
echo "INFO: Computed CNPG host paths were not provided; validated Synology storage roots only." >&2
fi
return 0
}
@ -226,14 +310,29 @@ apply_cnpg_selector_overrides() {
return 1
fi
local data_selector_json="${PROLE_CNPG_DATA_SELECTOR_JSON:-}"
local wal_selector_json="${PROLE_CNPG_WAL_SELECTOR_JSON:-}"
if [[ -z "$data_selector_json" || -z "$wal_selector_json" ]]; then
return 0
local expected_ns="$NAMESPACE"
local expected_cluster="$CNPG_CLUSTER_NAME"
if [[ -z "$expected_ns" || -z "$expected_cluster" ]]; then
echo "ERROR: Unable to derive namespace/cluster for CNPG selector injection (namespace='${expected_ns:-<missing>}', cluster='${expected_cluster:-<missing>}')." >&2
return 1
fi
local data_selector_json="${CNPG_DATA_SELECTOR_JSON:-}"
local wal_selector_json="${CNPG_WAL_SELECTOR_JSON:-}"
if [[ -z "$data_selector_json" ]]; then
data_selector_json='{}'
fi
if [[ -z "$wal_selector_json" ]]; then
wal_selector_json='{}'
fi
local patch_json patched
if ! patch_json=$(jq -cn --argjson data "$data_selector_json" --argjson wal "$wal_selector_json" '{"spec":{"storage":{"pvcTemplate":{"selector":{"matchLabels":$data}}},"walStorage":{"pvcTemplate":{"selector":{"matchLabels":$wal}}}}}'); then
if ! patch_json=$(jq -cn \
--argjson data "$data_selector_json" \
--argjson wal "$wal_selector_json" \
--arg ns "$expected_ns" \
--arg cluster "$expected_cluster" \
'{"spec":{"storage":{"pvcTemplate":{"selector":{"matchLabels":((if ($data|type)=="object" then $data else {} end) + {"synology.storage/role":"data","prole.io/namespace":$ns,"prole.io/cluster":$cluster})}}},"walStorage":{"pvcTemplate":{"selector":{"matchLabels":((if ($wal|type)=="object" then $wal else {} end) + {"synology.storage/role":"wal","prole.io/namespace":$ns,"prole.io/cluster":$cluster})}}}}}'); then
echo "ERROR: Invalid CNPG selector JSON provided by installer (data/wal)." >&2
return 1
fi
@ -338,8 +437,8 @@ validate_cnpg_manifest_storage() {
echo "CNPG manifest storage selector (final data): role='${data_role:-<missing>}', namespace='${data_ns:-<missing>}', cluster='${data_cluster:-<missing>}'"
echo "CNPG manifest storage selector (final wal): role='${wal_role:-<missing>}', namespace='${wal_ns:-<missing>}', cluster='${wal_cluster:-<missing>}'"
if [[ "$data_sc" != "$PROLE_CNPG_STORAGE_CLASS" || "$wal_sc" != "$PROLE_CNPG_STORAGE_CLASS" ]]; then
echo "ERROR: CNPG Cluster manifest must set storageClassName=${PROLE_CNPG_STORAGE_CLASS} for both data and walStorage (got data='${data_sc:-<missing>}', wal='${wal_sc:-<missing>}')." >&2
if [[ "$data_sc" != "$CNPG_STORAGE_CLASS" || "$wal_sc" != "$CNPG_STORAGE_CLASS" ]]; then
echo "ERROR: CNPG Cluster manifest must set storageClassName=${CNPG_STORAGE_CLASS} for both data and walStorage (got data='${data_sc:-<missing>}', wal='${wal_sc:-<missing>}')." >&2
echo "Refusing deployment to prevent fallback to default storage class." >&2
return 1
fi
@ -370,7 +469,7 @@ validate_cnpg_runtime_storage() {
return 0
fi
local sc="$PROLE_CNPG_STORAGE_CLASS"
local sc="$CNPG_STORAGE_CLASS"
local cluster_json
cluster_json=$(kubectl -n "$NAMESPACE" get cluster "$CNPG_CLUSTER_NAME" -o json 2>/dev/null || true)
if [[ -z "$cluster_json" ]]; then
@ -1840,7 +1939,10 @@ cleanup_unintended_cnpg_services() {
ensure_prole_stack_resources() {
echo "Applying CloudNative-PG cluster and related resources ..."
wait_for_apiserver_ready 180
ensure_prole_protected_storage
if ! ensure_prole_protected_storage; then
echo "ERROR: CNPG storage validation failed; skipping CNPG deployment path." >&2
return 1
fi
cleanup_unintended_cnpg_services || true
local image_override="${CNPG_IMAGE:-${KNOE_DB_IMAGE:-}}"
local image=""
@ -2888,7 +2990,10 @@ restore_cnpg_cluster() {
ensure_cnpg_operator
pin_cnpg_controller
ensure_barman_plugin
ensure_prole_protected_storage
if ! ensure_prole_protected_storage; then
echo "ERROR: CNPG storage validation failed; aborting CNPG restore." >&2
return 1
fi
if kubectl -n "$NAMESPACE" get cluster "$CNPG_CLUSTER_NAME" >/dev/null 2>&1; then
echo "ERROR: CNPG cluster '$CNPG_CLUSTER_NAME' already exists in namespace '$NAMESPACE'." >&2
@ -3348,7 +3453,10 @@ deploy_cluster() {
pin_cnpg_controller
ensure_barman_plugin
ensure_prole_protected_storage
if ! ensure_prole_protected_storage; then
echo "ERROR: CNPG storage validation failed; aborting CNPG deploy." >&2
return 1
fi
sync_manifest_image "$image"

View File

@ -5,7 +5,7 @@ metadata:
name: knoe-db
spec:
instances: 3
imageName: k3d-prole-registry:5000/knoe-db:18-140
imageName: example.invalid:5000/knoe-db:0.0.0
postgresUID: 100
postgresGID: 101
maxSyncReplicas: 1

View File

@ -1510,9 +1510,9 @@ class ProleInstaller:
raw_server, raw_token = self._resolve_k3s_connection()
if raw_server:
env["PROLE_K3S_SERVER"] = raw_server
env["K3S_SERVER"] = raw_server
if raw_token:
env["PROLE_K3S_TOKEN"] = raw_token
env["K3S_TOKEN"] = raw_token
elif mode == "k3d":
# Ensure k3d dev clusters have a resolvable KUBECONFIG.
@ -1781,24 +1781,28 @@ class ProleInstaller:
)
provisioned = provision_cluster_storage(spec)
env["PROLE_CNPG_STORAGE_CLASS"] = spec.storage_class_name
env["PROLE_CNPG_DATA_SELECTOR_JSON"] = json.dumps(
env["CNPG_STORAGE_CLASS"] = spec.storage_class_name
env["CNPG_DATA_SELECTOR_JSON"] = json.dumps(
provisioned.data_selector,
separators=(",", ":"),
sort_keys=True,
)
env["PROLE_CNPG_WAL_SELECTOR_JSON"] = json.dumps(
env["CNPG_WAL_SELECTOR_JSON"] = json.dumps(
provisioned.wal_selector,
separators=(",", ":"),
sort_keys=True,
)
env["PROLE_CNPG_DATA_PV_NAME"] = provisioned.data_pv_name
env["PROLE_CNPG_WAL_PV_NAME"] = provisioned.wal_pv_name
env["CNPG_DATA_PV_NAME"] = provisioned.data_pv_name
env["CNPG_WAL_PV_NAME"] = provisioned.wal_pv_name
env["CNPG_DATA_HOST_PATH"] = provisioned.data_path
env["CNPG_WAL_HOST_PATH"] = provisioned.wal_path
env["SYNOLOGY_ROOTS"] = ",".join(spec.synology_roots)
self.log(
"[CNPG] Provisioned cluster storage "
f"namespace={namespace} cluster={cluster_name} node={node_name} "
f"data_pv={provisioned.data_pv_name} wal_pv={provisioned.wal_pv_name}"
f"data_pv={provisioned.data_pv_name} wal_pv={provisioned.wal_pv_name} "
f"data_path={provisioned.data_path} wal_path={provisioned.wal_path}"
)
# --------------------------------------------- authority / repair
@ -3302,17 +3306,17 @@ class ProleConsoleInstaller(ProleInstaller):
legacy["init_cluster.cluster_env"] = _expand_cfg_value(
sec.get("CLUSTER_ENV", ""), cfg_vars
)
if "PROLE_K3S_SERVER" in sec:
if "K3S_SERVER" in sec:
legacy["init_cluster.k3s_server_url"] = _expand_cfg_value(
sec.get("PROLE_K3S_SERVER", ""), cfg_vars
sec.get("K3S_SERVER", ""), cfg_vars
)
if "K3S_SERVER_URL" in sec and "init_cluster.k3s_server_url" not in legacy:
legacy["init_cluster.k3s_server_url"] = _expand_cfg_value(
sec.get("K3S_SERVER_URL", ""), cfg_vars
)
if "PROLE_K3S_TOKEN" in sec:
if "K3S_TOKEN" in sec:
legacy["init_cluster.k3s_token"] = _expand_cfg_value(
sec.get("PROLE_K3S_TOKEN", ""), cfg_vars
sec.get("K3S_TOKEN", ""), cfg_vars
)
if "K3S_TOKEN" in sec and "init_cluster.k3s_token" not in legacy:
legacy["init_cluster.k3s_token"] = _expand_cfg_value(
@ -3671,7 +3675,7 @@ class ProleConsoleInstaller(ProleInstaller):
host = _host_from_url(server)
if not host:
self.err(
"[WARN] k3s mode but K3S_SERVER_URL/PROLE_K3S_SERVER not set; cannot resolve registry."
"[WARN] k3s mode but K3S_SERVER_URL/K3S_SERVER not set; cannot resolve registry."
)
return None
host_registry = f"{host}:5000"
@ -3965,13 +3969,22 @@ class ProleConsoleInstaller(ProleInstaller):
if cnpg_eligible_nodes:
glob["CNPG_ELIGIBLE_NODES"] = ",".join(cnpg_eligible_nodes)
synology_roots = sorted(
{
str((details or {}).get("path") or "").strip()
for details in mounts.values()
if isinstance(details, dict)
and str((details or {}).get("path") or "").strip()
}
)
if synology_roots:
glob["SYNOLOGY_ROOTS"] = ",".join(synology_roots)
# CNPG protected storage defaults (k3s local PV)
d001 = mounts.get("d001") or {}
d001_base = (d001.get("path") or "").strip()
d001_host = (d001.get("host") or "").strip()
if d001_base and d001_host:
glob["PROLE_PROTECTED_DATA_PATH"] = f"{d001_base}/data"
glob["PROLE_PROTECTED_WAL_PATH"] = f"{d001_base}/wal"
glob["CNPG_STAGE1_NODE"] = d001_host
glob["CNPG_DB_NODE_SELECTOR"] = f"kubernetes.io/hostname={d001_host}"
@ -3985,7 +3998,7 @@ class ProleConsoleInstaller(ProleInstaller):
mon_base = (mon_choice.get("path") or "").strip()
mon_host = (mon_choice.get("host") or "").strip()
if mon_base:
glob["PROLE_MONITORING_DATA_DIR"] = mon_base
glob["MONITORING_DATA_DIR"] = mon_base
if mon_host:
self.prole_cfg_data.setdefault("Monitoring", {})[
"MONITORING_PRIMARY_NODE"
@ -4072,17 +4085,17 @@ class ProleConsoleInstaller(ProleInstaller):
self._get_input("init_password.db_host_port", "5432") or "5432"
).strip(),
"DOCKER_IMPORT_DIR": self.docker_import_dir or "",
"PROLE_K3S_SERVER": (
"K3S_SERVER": (
self._get_input("init_cluster.k3s_server_url", "") or ""
).strip(),
"PROLE_K3S_TOKEN": self._secret_cfg_value(
"K3S_TOKEN": self._secret_cfg_value(
"Global",
"PROLE_K3S_TOKEN",
"K3S_TOKEN",
self._get_input("init_cluster.k3s_token", ""),
"k3s",
"token",
),
"PROLE_OPENTOFU_URL": _default_opentofu_pipeline_url(),
"OPENTOFU_URL": _default_opentofu_pipeline_url(),
"SERVICE_NAMESPACE": self._service_namespace(),
}
# Merge pre-existing Global values without overriding explicit inputs
@ -4432,14 +4445,14 @@ class ProleConsoleInstaller(ProleInstaller):
or _cfg_val(sys_sec, "PROLE_SERVICE")
or _cfg_val(global_sec, "PROLE_SERVICE")
),
"PROLE_OPENTOFU_URL": (os.environ.get("PROLE_OPENTOFU_URL") or "").strip(),
"OPENTOFU_URL": (os.environ.get("OPENTOFU_URL") or "").strip(),
}
defaults = self._env_defaults(ns_hint)
for k in vals:
if not vals[k]:
vals[k] = defaults.get(k, "")
if not vals.get("PROLE_OPENTOFU_URL"):
vals["PROLE_OPENTOFU_URL"] = _default_opentofu_pipeline_url()
if not vals.get("OPENTOFU_URL"):
vals["OPENTOFU_URL"] = _default_opentofu_pipeline_url()
vals["NAMESPACE"] = self._get_input("env_setup.NAMESPACE", "") or ns_hint
if not vals.get("PROLE_HOME"):
@ -4450,7 +4463,7 @@ class ProleConsoleInstaller(ProleInstaller):
"PROLE_CONF",
"PROLE_DATA",
"PROLE_LOGS",
"PROLE_SERVICE",
"ROLE_SERVICE",
):
self.inputs[f"env_setup.{k}"] = vals[k]
self.inputs["env_setup.NAMESPACE"] = vals.get("NAMESPACE", "")
@ -4463,7 +4476,7 @@ class ProleConsoleInstaller(ProleInstaller):
"PROLE_DATA",
"PROLE_LOGS",
"PROLE_SERVICE",
"PROLE_OPENTOFU_URL",
"OPENTOFU_URL",
):
self.prole_cfg_data["System Environment"][k] = vals[k]
@ -5364,6 +5377,12 @@ class ProleConsoleInstaller(ProleInstaller):
self.err(f"[ERROR] Failed to ensure DB secrets before CNPG init: {e}")
overall_success = False
break
try:
self._ensure_cnpg_storage_provisioned(ns, env)
except Exception as e:
self.err(f"[ERROR] Failed to provision CNPG storage before init: {e}")
overall_success = False
break
# Use custom line handler for monitoring to capture Grafana password and port mappings
rc = self._run_script(
@ -6124,7 +6143,7 @@ def _prepare_k3s_pipeline(
attempt = 1
total_attempts = 0
max_attempts_env = os.environ.get("PROLE_SILENT_TEST_MAX_ATTEMPTS", "").strip()
max_attempts_env = os.environ.get("SILENT_TEST_MAX_ATTEMPTS", "").strip()
max_attempts = int(max_attempts_env) if max_attempts_env.isdigit() else 0
_log("==> Running silent install validation...\n")

View File

@ -27,6 +27,7 @@ class ClusterNodesScreenMixin:
("cloudnativepg", "CloudNative-PG"),
("pihole", "Pi-hole"),
]
ADVANCED_ASSIGNMENT_GRID_KEY = "ADVANCED_ASSIGNMENT_GRID"
def _render_cluster_nodes_page(self):
# Gate: only for multi-node, non-k3d.
@ -67,6 +68,79 @@ class ClusterNodesScreenMixin:
)
return
advanced_enabled = self._cluster_nodes_advanced_enabled()
advanced_var = tk.BooleanVar(master=self.root, value=advanced_enabled)
def _toggle_advanced_grid():
enabled = bool(advanced_var.get())
self._cluster_nodes_set_advanced_enabled(enabled)
if not enabled:
default_policy = self._cluster_nodes_default_policy(hosts)
self._cluster_nodes_current_policy = default_policy
self._validate_and_save_cluster_nodes_policy(default_policy)
self._render_cluster_nodes_page()
advanced_chk = tk.Checkbutton(
self.bg_canvas,
text="Enable advanced assignment grid",
variable=advanced_var,
command=_toggle_advanced_grid,
bg="white",
activebackground="white",
font=("SF Pro Text", 11),
anchor="w",
)
advanced_win = self.bg_canvas.create_window(48, 238, window=advanced_chk, anchor="nw", width=360)
self._canvas_items.append(advanced_win)
self._overlay_widgets.append(advanced_chk)
if not advanced_enabled:
policy = self._cluster_nodes_default_policy(hosts)
self._cluster_nodes_current_policy = policy
monitoring_host = ((policy.get("defaults") or {}).get("monitoring_host") or "last host")
self._render_paragraph(
"Simple placement mode is active. The advanced assignment grid is disabled by default.",
y=280,
wrap=860,
)
ui.canvas_text(
self,
48,
330,
f"Default policy: CloudNative-PG uses round-robin allocation; monitoring defaults follow /synology/d004 (host: {monitoring_host}).",
fill="#1d1d1f",
font=("SF Pro Text", 11),
)
def _save_simple():
self._cluster_nodes_set_advanced_enabled(False)
if not self._validate_and_save_cluster_nodes_policy(policy):
return
try:
messagebox.showinfo("Cluster Nodes", "Saved default cluster node policy.")
except Exception:
pass
btn = tk.Button(
self.bg_canvas,
text="Save",
command=_save_simple,
bg="#F5F5DC",
fg="black",
activebackground="#E5E5D5",
highlightbackground="#F5F5DC",
highlightthickness=0,
relief="flat",
font=("SF Pro Text", 11),
padx=14,
pady=6,
)
btn_win = self.bg_canvas.create_window(48, 380, window=btn, anchor="nw", width=180)
self._canvas_items.append(btn_win)
self._overlay_widgets.append(btn)
return
policy = self._cluster_nodes_load_policy(hosts)
# Keep in instance state for navigation (Next button) saves.
self._cluster_nodes_current_policy = policy
@ -362,19 +436,55 @@ class ClusterNodesScreenMixin:
except Exception:
pass
# Default: enabled everywhere, no primary.
return self._cluster_nodes_default_policy(hosts)
def _cluster_nodes_advanced_enabled(self) -> bool:
sec = (getattr(self, "prole_cfg_data", None) or {}).get("Cluster Nodes", {})
raw = (sec or {}).get(self.ADVANCED_ASSIGNMENT_GRID_KEY, False)
if isinstance(raw, bool):
return raw
return str(raw).strip().lower() in {"1", "true", "yes", "on"}
def _cluster_nodes_set_advanced_enabled(self, enabled: bool) -> None:
if "Cluster Nodes" not in self.prole_cfg_data:
self.prole_cfg_data["Cluster Nodes"] = {}
self.prole_cfg_data["Cluster Nodes"][self.ADVANCED_ASSIGNMENT_GRID_KEY] = "true" if enabled else "false"
try:
self._save_prole_cfg()
except Exception:
pass
def _cluster_nodes_monitoring_host(self, hosts: list[str]) -> str:
for host in hosts:
if "d004" in host.lower():
return host
return hosts[-1] if hosts else ""
def _cluster_nodes_default_policy(self, hosts: list[str]) -> dict:
services = {}
for sid, _title in self.CLUSTER_NODE_SERVICES:
services[sid] = {"primary_host": "", "enabled_hosts": list(hosts)}
return {
"defaults": {
"cnpg_allocation": "round-robin",
"monitoring_storage_root": "/synology/d004",
"monitoring_host": self._cluster_nodes_monitoring_host(hosts),
},
"hosts": {h: {"protected": False, "reserved_hostports": False} for h in hosts},
"services": services,
}
def _cluster_nodes_normalize_policy(self, pol: dict, hosts: list[str]) -> dict:
out = {"hosts": {}, "services": {}}
out = {"defaults": {}, "hosts": {}, "services": {}}
host_set = set(hosts)
defaults_in = pol.get("defaults") if isinstance(pol.get("defaults"), dict) else {}
out["defaults"] = {
"cnpg_allocation": str(defaults_in.get("cnpg_allocation") or "round-robin"),
"monitoring_storage_root": str(defaults_in.get("monitoring_storage_root") or "/synology/d004"),
"monitoring_host": str(defaults_in.get("monitoring_host") or self._cluster_nodes_monitoring_host(hosts)),
}
for h in hosts:
hpol = ((pol.get("hosts") or {}).get(h) or {}) if isinstance(pol.get("hosts"), dict) else {}
out["hosts"][h] = {

View File

@ -0,0 +1 @@
18

View File

@ -0,0 +1 @@
2

View File

@ -1,53 +1,52 @@
Network Discovery Summary:
Primary Router: 10.0.0.1 (eero_5d:50:f2)
DNS Servers: 100.100.100.100, 10.0.0.5, 10.0.0.4
DNS Servers: 10.0.0.5, 10.0.0.4, 100.100.100.100
Detected Devices:
- 10.0.0.38 [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.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.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.205 [a8:20:66:28:12:e7] (Apple_28:12:e7): Ports [22, 445, 5900, 88], Services: ['SSH', 'VNC', 'SMB/CIFS (Possible Windows/AD)', 'Active Directory Related']
- 10.0.0.1 [9c:57:bc:5d:50:f2] (eero_5d:50:f2): Ports [53], Services: ['DNS']
- 10.0.0.38 [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.5 [b8:27:eb:88:3a:41] (RaspberryPiF_88:3a:41): Ports [22, 53, 443, 2049, 5900], Services: ['DNS', 'SSH', 'NFS', 'VNC', 'Web Server']
- 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.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.100 [24:fc:e5:51:cf:74] (SamsungElect_51:cf:74): 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.33 [ec:b5:fa:b0:76:e4] (PhilipsLight_b0:76:e4): Ports [80, 443], Services: ['Web Server']
- 10.0.0.37 [54:07:7d:22:c0:b2] (Netgear_22:c0:b2): Ports [80], Services: ['Web Server']
- 10.0.0.45 [f8:b4:6a:30:a4:1f] (HewlettPacka_30:a4:1f): Ports [80, 443], Services: ['Web Server']
- 10.0.0.48 [0c:fe:45:53:f4:3c] (SonyInteract_53:f4:3c): Ports [], Services: []
- 10.0.0.55 [48:a6:b8:a7:50:60] (Sonos_a7:50:60): Ports [], Services: []
- 10.0.0.46 [d4:f7:d5:40:ab:17] (SonyInteract_40:ab:17): Ports [], Services: []
- 10.0.0.41 [b8:27:eb:57:10:d9] (RaspberryPiF_57:10:d9): Ports [22], Services: ['SSH']
- 10.0.0.206 [00:a0:de:a2:0b:ef] (Yamaha_a2:0b:ef): Ports [80], Services: ['Web Server']
- 10.0.0.46 [d4:f7:d5:40:ab:17] (SonyInteract_40:ab:17): Ports [], Services: []
- 10.0.0.73 [4c:a9:19:b3:12:f8] (TuyaSmart_b3:12:f8): Ports [], Services: []
- 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.107 [b8:06:0d:b7:7c:56] (TuyaSmart_b7:7c:56): 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.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 [22, 53, 443, 2049, 5900], Services: ['DNS', 'SSH', 'NFS', 'VNC', 'Web Server']
- 10.0.0.99 [20:28:bc:f1:dc:ba] (Visionscape_f1:dc:ba): 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.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.111 [ca:0b:6c:5d:0a:e5] (ca:0b:6c:5d:0a:e5): Ports [], Services: []
- 10.0.0.127 [58:55:ca:4a:60:6e] (Apple_4a:60:6e): Ports [22], Services: ['SSH']
- 10.0.0.155 [62:17:97:b3:06:a1] (62:17:97:b3:06:a1): Ports [], Services: []
- 10.0.0.196 [60:5f:8d:88:08:f2] (eero_88:08:f2): 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.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.130 [f8:bb:bf:a3:84:eb] (eero_a3:84:eb): Ports [53], Services: ['DNS']
- 10.0.0.113 [60:81:10:92:02:4e] (Apple_92:02:4e): Ports [], Services: []
- 10.0.0.127 [58:55:ca:4a:60:6e] (Apple_4a:60:6e): Ports [22], Services: ['SSH']
- 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 [80], Services: ['Web Server']
- 10.0.0.128 [20:c9:d0:94:30:dd] (Apple_94:30:dd): Ports [22, 445, 5900, 88], Services: ['SSH', 'VNC', 'SMB/CIFS (Possible Windows/AD)', 'Active Directory Related']
- 10.0.0.130 [f8:bb:bf:a3:84:eb] (eero_a3:84:eb): Ports [53], Services: ['DNS']
- 10.0.0.112 [f8:33:31:40:58:c0] (TexasInstrum_40:58:c0): Ports [80, 443], Services: ['Web Server']
- 10.0.0.128 [20:c9:d0:94:30:dd] (Apple_94:30:dd): Ports [88], Services: ['Active Directory Related']
- 10.0.0.205 [a8:20:66:28:12:e7] (Apple_28:12:e7): Ports [22, 445, 5900, 88], Services: ['SSH', 'VNC', 'SMB/CIFS (Possible Windows/AD)', 'Active Directory Related']
- 10.0.0.179 [48:a6:b8:a4:8e:cc] (Sonos_a4:8e:cc): Ports [], Services: []
- 10.0.0.143 [60:5f:8d:7a:aa:32] (eero_7a:aa:32): Ports [53], Services: ['DNS']
- 10.0.0.145 [7c:a6:b0:04:db:1c] (7c:a6:b0:04:db:1c): Ports [], Services: []
- 10.0.0.206 [00:a0:de:a2:0b:ef] (Yamaha_a2:0b:ef): Ports [80], Services: ['Web Server']
- 10.0.0.1\ [9c:57:bc:5d:50:f2] (eero_5d:50:f2): Ports [], Services: []
- 10.0.0.170 [b0:ee:7b:ca:73:99] (Roku_ca:73:99): Ports [], Services: []
- 10.0.0.175 [28:80:88:e4:10:1a] (Netgear_e4:10:1a): Ports [80], Services: ['Web Server']
- 10.0.0.179 [48:a6:b8:a4:8e:cc] (Sonos_a4:8e:cc): 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.188 [00:16:6c:c5:1f:54] (SamsungElect_c5:1f:54): Ports [443], Services: ['Web Server']
- 10.0.0.208 [a0:ad:9f:30:84:5f] (ASUSTekCOMPU_30:84:5f): Ports [], Services: []
- 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.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.196 [60:5f:8d:88:08:f2] (eero_88:08:f2): Ports [53], Services: ['DNS']
- 10.0.0.188 [00:16:6c:c5:1f:54] (SamsungElect_c5:1f:54): Ports [80, 443], Services: ['Web Server']
- 10.0.0.38\ [7e:19:9a:fe:9f:ed] (7e:19:9a:fe:9f:ed): Ports [], Services: []
- 10.0.0.208 [a0:ad:9f:30:84:5f] (ASUSTekCOMPU_30:84:5f): Ports [], Services: []
Ollama Instances found at: 10.0.0.38, 10.0.0.204
Ansible Inventory Summary:

View File

@ -3,9 +3,10 @@
set -euo pipefail
# Regression tests for k3s CNPG storage guardrails in etc/init_cloudnative_pg.sh
# - must fail if protected mounts are root-backed
# - must fail if Synology storage roots are root-backed
# - must fail if manifest lacks explicit synology-iscsi + selector labels (prevents local-path fallback)
# - must succeed (with mocks) when mounts + manifest + runtime objects are correct
# - must fail if a required Synology root is missing
# - must succeed (with mocks) when mounts + computed paths + manifest + runtime objects are correct
SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
REPO_ROOT=$(cd "$SCRIPT_DIR/../.." && pwd)
@ -61,10 +62,25 @@ if [[ "$field" == "TARGET" ]]; then
exit 0
fi
case "$path" in
"${PROLE_PROTECTED_DATA_PATH}") printf '%s\n' "${PROLE_TEST_FINDMNT_DATA_SRC:-/dev/iscsi-data}"; exit 0 ;;
"${PROLE_PROTECTED_WAL_PATH}") printf '%s\n' "${PROLE_TEST_FINDMNT_WAL_SRC:-/dev/iscsi-wal}"; exit 0 ;;
esac
if [[ -n "${PROLE_TEST_ROOT_D001:-}" && "$path" == "${PROLE_TEST_ROOT_D001}"* ]]; then
printf '%s\n' "${PROLE_TEST_FINDMNT_D001_SRC:-/dev/iscsi-d001}"
exit 0
fi
if [[ -n "${PROLE_TEST_ROOT_D002:-}" && "$path" == "${PROLE_TEST_ROOT_D002}"* ]]; then
printf '%s\n' "${PROLE_TEST_FINDMNT_D002_SRC:-/dev/iscsi-d002}"
exit 0
fi
if [[ -n "${PROLE_TEST_ROOT_D003:-}" && "$path" == "${PROLE_TEST_ROOT_D003}"* ]]; then
printf '%s\n' "${PROLE_TEST_FINDMNT_D003_SRC:-/dev/iscsi-d003}"
exit 0
fi
if [[ -n "${PROLE_TEST_ROOT_D004:-}" && "$path" == "${PROLE_TEST_ROOT_D004}"* ]]; then
printf '%s\n' "${PROLE_TEST_FINDMNT_D004_SRC:-/dev/iscsi-d004}"
exit 0
fi
# Default: pretend it's root-backed
printf '%s\n' "${PROLE_TEST_FINDMNT_ROOT_SRC:-/dev/root}"
@ -82,6 +98,40 @@ fi
args="$*"
# Simulate kubectl local merge patch used for selector injection.
if [[ "${args}" == patch*" --local "*" -o yaml"* ]]; then
src=""
if [[ "$#" -ge 2 ]]; then
for ((i=1; i<=$#; i++)); do
if [[ "${!i}" == "-f" ]]; then
j=$((i+1))
src="${!j:-}"
break
fi
done
fi
if [[ -n "${src}" && -f "${src}" ]]; then
awk '
{
print
if ($0 ~ /^[[:space:]]*synology\.storage\/role:[[:space:]]*data[[:space:]]*$/) {
match($0, /^[[:space:]]*/)
indent = substr($0, RSTART, RLENGTH)
print indent "prole.io/namespace: test-ns"
print indent "prole.io/cluster: knoe-db"
}
if ($0 ~ /^[[:space:]]*synology\.storage\/role:[[:space:]]*wal[[:space:]]*$/) {
match($0, /^[[:space:]]*/)
indent = substr($0, RSTART, RLENGTH)
print indent "prole.io/namespace: test-ns"
print indent "prole.io/cluster: knoe-db"
}
}
' "${src}"
fi
exit 0
fi
if [[ "$args" == *"get --raw='/readyz'"* || "$args" == *"get --raw=/readyz"* ]]; then
echo "ok"
exit 0
@ -145,7 +195,7 @@ if [[ "$args" == *" get cluster knoe-db -o jsonpath="* ]]; then
fi
if [[ "$args" == *" get cluster knoe-db -o json"* ]]; then
cat <<JSON
{"apiVersion":"postgresql.cnpg.io/v1","kind":"Cluster","metadata":{"name":"knoe-db"},"spec":{"storage":{"pvcTemplate":{"storageClassName":"synology-iscsi","selector":{"matchLabels":{"synology.storage/role":"data"}}}},"walStorage":{"pvcTemplate":{"storageClassName":"synology-iscsi","selector":{"matchLabels":{"synology.storage/role":"wal"}}}}}}
{"apiVersion":"postgresql.cnpg.io/v1","kind":"Cluster","metadata":{"name":"knoe-db"},"spec":{"storage":{"pvcTemplate":{"storageClassName":"synology-iscsi","selector":{"matchLabels":{"synology.storage/role":"data","prole.io/namespace":"test-ns","prole.io/cluster":"knoe-db"}}}},"walStorage":{"pvcTemplate":{"storageClassName":"synology-iscsi","selector":{"matchLabels":{"synology.storage/role":"wal","prole.io/namespace":"test-ns","prole.io/cluster":"knoe-db"}}}}}}
JSON
exit 0
fi
@ -216,15 +266,25 @@ EOF
local MANIFEST_DIR="$PROLE_HOME/deploy/opentofu/k3s/manifests/prole"
mkdir -p "$MANIFEST_DIR"
# Storage paths (override to tmp)
export PROLE_PROTECTED_DATA_PATH="$TMP_DIR/prole-mount/data"
export PROLE_PROTECTED_WAL_PATH="$TMP_DIR/prole-mount/wal"
mkdir -p "$PROLE_PROTECTED_DATA_PATH" "$PROLE_PROTECTED_WAL_PATH"
# Synology roots + computed host paths (override to tmp)
export PROLE_TEST_ROOT_D001="$TMP_DIR/synology/d001"
export PROLE_TEST_ROOT_D002="$TMP_DIR/synology/d002"
export PROLE_TEST_ROOT_D003="$TMP_DIR/synology/d003"
export PROLE_TEST_ROOT_D004="$TMP_DIR/synology/d004"
mkdir -p "$PROLE_TEST_ROOT_D001" "$PROLE_TEST_ROOT_D002" "$PROLE_TEST_ROOT_D003" "$PROLE_TEST_ROOT_D004"
export SYNOLOGY_ROOTS="$PROLE_TEST_ROOT_D001,$PROLE_TEST_ROOT_D002,$PROLE_TEST_ROOT_D003,$PROLE_TEST_ROOT_D004"
export CNPG_DATA_HOST_PATH="$PROLE_TEST_ROOT_D001/knoe/knoe-db/test-ns/knoe-db/data"
export CNPG_WAL_HOST_PATH="$PROLE_TEST_ROOT_D002/knoe/knoe-db/test-ns/knoe-db/wal"
mkdir -p "$CNPG_DATA_HOST_PATH" "$CNPG_WAL_HOST_PATH"
export CNPG_STAGE1_NODE="$(hostname -f 2>/dev/null || hostname)"
# Defaults: non-root-backed mounts
export PROLE_TEST_FINDMNT_ROOT_SRC="/dev/root"
export PROLE_TEST_FINDMNT_DATA_SRC="/dev/iscsi0"
export PROLE_TEST_FINDMNT_WAL_SRC="/dev/iscsi1"
export PROLE_TEST_FINDMNT_D001_SRC="/dev/iscsi0"
export PROLE_TEST_FINDMNT_D002_SRC="/dev/iscsi1"
export PROLE_TEST_FINDMNT_D003_SRC="/dev/iscsi2"
export PROLE_TEST_FINDMNT_D004_SRC="/dev/iscsi3"
# Run the case-provided setup (writes manifest + adjusts mocks)
"$@" "$MANIFEST_DIR" "$TMP_DIR"
@ -264,8 +324,32 @@ spec:
synology.storage/role: wal
EOF
export PROLE_TEST_FINDMNT_DATA_SRC="/dev/root"
export PROLE_TEST_FINDMNT_WAL_SRC="/dev/root"
export PROLE_TEST_FINDMNT_D001_SRC="/dev/root"
}
case_missing_root() {
local manifest_dir="$1"
cat >"$manifest_dir/knoe-db.yaml" <<'EOF'
apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata:
name: knoe-db
spec:
storage:
pvcTemplate:
storageClassName: synology-iscsi
selector:
matchLabels:
synology.storage/role: data
walStorage:
pvcTemplate:
storageClassName: synology-iscsi
selector:
matchLabels:
synology.storage/role: wal
EOF
rm -rf "$PROLE_TEST_ROOT_D004"
}
case_invalid_manifest_missing_sc() {
@ -323,19 +407,25 @@ spec:
EOF
}
# 1) Root-backed mounts must hard-fail.
# 1) Root-backed storage roots must hard-fail.
if run_case "root-backed mounts" case_root_backed; then
echo "FAILURE: expected root-backed mounts to fail" >&2
exit 1
fi
# 2) Missing explicit storageClassName/selector must hard-fail.
# 2) Missing storage roots must hard-fail.
if run_case "missing Synology root" case_missing_root; then
echo "FAILURE: expected missing Synology root to fail" >&2
exit 1
fi
# 3) Missing explicit storageClassName/selector must hard-fail.
if run_case "invalid manifest (missing storageClassName/selector)" case_invalid_manifest_missing_sc; then
echo "FAILURE: expected invalid manifest to fail" >&2
exit 1
fi
# 3) Happy path should succeed with mocks.
# 4) Happy path should succeed with mocks.
run_case "success" case_success
echo "SUCCESS"

View File

@ -251,6 +251,14 @@ run_case() {
local cnpg_instances_cfg="${2:-}"
local mock_ready_nodes="${3:-1}"
local mock_db_nodes="${4:-1}"
local selector_mode="${5:-provided}"
local data_selector_env='{"synology.storage/role":"data","prole.io/namespace":"test-ns","prole.io/cluster":"knoe-db"}'
local wal_selector_env='{"synology.storage/role":"wal","prole.io/namespace":"test-ns","prole.io/cluster":"knoe-db"}'
if [[ "$selector_mode" == "missing" ]]; then
data_selector_env=""
wal_selector_env=""
fi
local conf_dir="$TMP_DIR/conf-${label}"
local env_home="$TMP_DIR/env-${label}"
@ -281,10 +289,11 @@ E_EOF
rm -f "$TMP_DIR/applied.yaml"
rm -f "$TMP_DIR/mock_cluster_instances"
rm -f "$TMP_DIR/mock_calls.log"
set +e
MOCK_READY_NODES="$mock_ready_nodes" MOCK_DB_NODES="$mock_db_nodes" \
PROLE_CNPG_DATA_SELECTOR_JSON='{"synology.storage/role":"data","prole.io/namespace":"test-ns","prole.io/cluster":"knoe-db"}' \
PROLE_CNPG_WAL_SELECTOR_JSON='{"synology.storage/role":"wal","prole.io/namespace":"test-ns","prole.io/cluster":"knoe-db"}' \
CNPG_DATA_SELECTOR_JSON="$data_selector_env" \
CNPG_WAL_SELECTOR_JSON="$wal_selector_env" \
BARMAN_CRD_TIMEOUT=5 BARMAN_TLS_TIMEOUT=5 BARMAN_DEPLOY_TIMEOUT=5 BARMAN_SERVICE_TIMEOUT=5 \
CNPG_STORAGE_VALIDATE_TIMEOUT=5 \
PROLE_HOME="$env_home" \
@ -307,9 +316,30 @@ E_EOF
fi
}
assert_applied_has_namespace_cluster_selectors() {
local label="$1"
local ns_count cluster_count
ns_count=$(grep -cE '^\s*prole\.io/namespace:\s*test-ns\s*$' "$TMP_DIR/applied.yaml" || true)
cluster_count=$(grep -cE '^\s*prole\.io/cluster:\s*knoe-db\s*$' "$TMP_DIR/applied.yaml" || true)
if (( ns_count < 2 )); then
echo "FAILURE: expected namespace selectors for both data and wal in case '${label}'"
sed -n '1,200p' "$TMP_DIR/applied.yaml" || true
exit 1
fi
if (( cluster_count < 2 )); then
echo "FAILURE: expected cluster selectors for both data and wal in case '${label}'"
sed -n '1,200p' "$TMP_DIR/applied.yaml" || true
exit 1
fi
}
# Default: manifest must be label-based (no hostname pinning) and reconcile must scale down to 1
# when only one node is Ready/schedulable.
run_case "default" "" 1 1
assert_applied_has_namespace_cluster_selectors "default"
if grep -qE '^\s*- key:\s*kubernetes\\.io/hostname\s*$|myrddin\\.prole\\.org' "$TMP_DIR/applied.yaml"; then
echo "FAILURE: expected no hostname pinning in applied manifest (default)"
sed -n '1,160p' "$TMP_DIR/applied.yaml" || true
@ -341,6 +371,16 @@ if [[ "$(cat "$TMP_DIR/mock_cluster_instances" 2>/dev/null || true)" != "1" ]];
exit 1
fi
# Regression: selector JSON env vars may be absent in direct shell invocation, but
# manifest must still be patched with namespace+cluster selector labels before validation.
run_case "no-selector-env" "" 1 1 "missing"
assert_applied_has_namespace_cluster_selectors "no-selector-env"
if ! grep -q "patch --local" "$TMP_DIR/mock_calls.log"; then
echo "FAILURE: expected local CNPG selector patch to run in case 'no-selector-env'"
sed -n '1,200p' "$TMP_DIR/mock_calls.log" || true
exit 1
fi
# Override: when capacity allows (2 ready db nodes), CNPG_INSTANCES must be honored (cap=2)
run_case "override" "CNPG_INSTANCES = 2" 2 2
if [[ "$(cat "$TMP_DIR/mock_cluster_instances" 2>/dev/null || true)" != "2" ]]; then

View File

@ -235,6 +235,10 @@ mkdir -p "$TMP_DIR/service/secrets"
printf '%s' "dummy-private-key" > "$TMP_DIR/service/secrets/admin.key"
printf '%s' "dummy-public-key" > "$TMP_DIR/service/secrets/admin.pub"
mkdir -p "$TMP_DIR/synology/d001" "$TMP_DIR/synology/d002" "$TMP_DIR/synology/d003" "$TMP_DIR/synology/d004"
mkdir -p "$TMP_DIR/synology/d001/knoe/knoe-db/test-ns/knoe-db/data"
mkdir -p "$TMP_DIR/synology/d002/knoe/knoe-db/test-ns/knoe-db/wal"
CONF_DIR="$TMP_DIR/conf"
ENV_HOME="$TMP_DIR/env"
mkdir -p "$CONF_DIR" "$ENV_HOME"
@ -260,6 +264,9 @@ chmod +x "$ENV_HOME/env.sh"
set +e
PROLE_HOME="$ENV_HOME" \
SYNOLOGY_ROOTS="$TMP_DIR/synology/d001,$TMP_DIR/synology/d002,$TMP_DIR/synology/d003,$TMP_DIR/synology/d004" \
CNPG_DATA_HOST_PATH="$TMP_DIR/synology/d001/knoe/knoe-db/test-ns/knoe-db/data" \
CNPG_WAL_HOST_PATH="$TMP_DIR/synology/d002/knoe/knoe-db/test-ns/knoe-db/wal" \
BARMAN_CRD_TIMEOUT=5 BARMAN_TLS_TIMEOUT=5 BARMAN_DEPLOY_TIMEOUT=5 BARMAN_SERVICE_TIMEOUT=5 \
CNPG_WAIT_TIMEOUT=5 CNPG_STORAGE_VALIDATE_TIMEOUT=5 \
bash "$SCRIPT_UNDER_TEST" --mode k3s initialize >"$TMP_DIR/stdout.log" 2>"$TMP_DIR/stderr.log"

View File

@ -781,15 +781,15 @@ class TestStepCnpgDeploy:
calls["provision"] += 1
assert ns == "knoe-db"
assert env_arg is env
env_arg["PROLE_CNPG_DATA_SELECTOR_JSON"] = '{"synology.storage/role":"data"}'
env_arg["PROLE_CNPG_WAL_SELECTOR_JSON"] = '{"synology.storage/role":"wal"}'
env_arg["CNPG_DATA_SELECTOR_JSON"] = '{"synology.storage/role":"data"}'
env_arg["CNPG_WAL_SELECTOR_JSON"] = '{"synology.storage/role":"wal"}'
def _fake_run(cmd, env=None, **_kwargs):
calls["run"] += 1
assert "init_cloudnative_pg.sh" in str(cmd[1])
assert env is not None
assert env["PROLE_CNPG_DATA_SELECTOR_JSON"]
assert env["PROLE_CNPG_WAL_SELECTOR_JSON"]
assert env["CNPG_DATA_SELECTOR_JSON"]
assert env["CNPG_WAL_SELECTOR_JSON"]
return 0
monkeypatch.setattr(inst, "_ensure_cnpg_storage_provisioned", _fake_provision)

View File

@ -0,0 +1,52 @@
from __future__ import annotations
from knoe.ui.screens.cluster_nodes import ClusterNodesScreenMixin
class _DummyClusterNodesScreen(ClusterNodesScreenMixin):
def __init__(self, prole_cfg_data: dict | None = None):
self.prole_cfg_data = prole_cfg_data or {}
self._save_calls = 0
def _save_prole_cfg(self):
self._save_calls += 1
def test_advanced_assignment_grid_disabled_by_default():
app = _DummyClusterNodesScreen(prole_cfg_data={"Cluster Nodes": {}})
assert app._cluster_nodes_advanced_enabled() is False
def test_set_advanced_assignment_grid_flag_persists_setting():
app = _DummyClusterNodesScreen(prole_cfg_data={"Cluster Nodes": {}})
app._cluster_nodes_set_advanced_enabled(True)
assert app.prole_cfg_data["Cluster Nodes"]["ADVANCED_ASSIGNMENT_GRID"] == "true"
assert app._save_calls == 1
def test_default_policy_uses_round_robin_cnpg_and_prefers_d004_for_monitoring():
app = _DummyClusterNodesScreen(prole_cfg_data={})
policy = app._cluster_nodes_default_policy(["node-a", "node-d004", "node-c"])
assert policy["defaults"]["cnpg_allocation"] == "round-robin"
assert policy["defaults"]["monitoring_storage_root"] == "/synology/d004"
assert policy["defaults"]["monitoring_host"] == "node-d004"
assert policy["services"]["cloudnativepg"]["primary_host"] == ""
assert policy["services"]["cloudnativepg"]["enabled_hosts"] == [
"node-a",
"node-d004",
"node-c",
]
def test_load_policy_falls_back_to_default_when_policy_json_missing():
app = _DummyClusterNodesScreen(prole_cfg_data={"Cluster Nodes": {}})
policy = app._cluster_nodes_load_policy(["node-a", "node-b"])
assert policy["defaults"]["cnpg_allocation"] == "round-robin"
assert policy["defaults"]["monitoring_storage_root"] == "/synology/d004"
assert policy["defaults"]["monitoring_host"] == "node-b"
assert policy["services"]["cloudnativepg"]["primary_host"] == ""