fix(cnpg): stabilize CNPG/Barman Cloud init on GKE (knoe-dev-0 / knoe-db-0)

- cloudnative_pg.py: add _wait_for_webhook_ca_bundle() to poll ValidatingWebhookConfiguration
  until cainjector has injected the CA bundle; fall back to _try_inject_ca_bundle_manually()
  on timeout (reads cert-manager-webhook-ca secret, patches webhook directly)
- cloudnative_pg.py: add _wait_for_barman_tls_secrets() to wait for barman-cloud-{client,server}-tls
  before declaring Barman Cloud ready; surfaces warning instead of silent FailedMount
- deploy/gcp/gke/namespace.yaml: ecosystem-0 -> knoe-db-0 (ekosystem = k8s cluster, not namespace)
- deploy/gcp/gke/knoe-db.yaml: fix namespace ecosystem-0 -> knoe-db-0; set
  podAntiAffinityType: required (one CNPG pod per node); add Spot node toleration so
  GKE autoscaler simulation succeeds and scale-up is unblocked; add resource
  requests/limits (500m/512Mi -> 2/2Gi); set googleCredentials.gkeEnvironment: true
  (use Workload Identity - no explicit credentials secret needed)
- deploy/gcp/gke/knoe-db-backup-gcs.yaml: fix namespaces ecosystem-0 -> knoe-db-0
- deploy/gcp/gke/workload-identity.yaml: update comments ecosystem-0 -> knoe-db-0
- tests/installer/test_cnpg_barman_webhook.py: new - 13 tests covering webhook CA bundle
  polling, manual CA injection, Barman TLS secret wait, and install_barman_plugin retry logic

Co-authored-by: Junie <junie@jetbrains.com>
This commit is contained in:
chrisfu 2026-04-05 02:04:11 -07:00
parent 3312c39b1f
commit c165ee1a87
14 changed files with 567 additions and 115 deletions

View File

@ -1,23 +1,23 @@
{
"assignments": {
"0": "gk3-knoe-dev-0-pool-1-11c87d92-8qr5",
"1": "gk3-knoe-dev-0-pool-1-88b7392b-62c4",
"2": "gk3-knoe-dev-0-pool-1-88b7392b-d6lb"
"0": "k3d-knoe-dev-cluster-agent-0",
"1": "k3d-knoe-dev-cluster-agent-1",
"2": "k3d-knoe-dev-cluster-server-0"
},
"cluster_name": "knoe-db",
"desired_instances": 3,
"eligible_nodes": [
"gk3-knoe-dev-0-pool-1-11c87d92-8qr5",
"gk3-knoe-dev-0-pool-1-88b7392b-62c4",
"gk3-knoe-dev-0-pool-1-88b7392b-d6lb"
"k3d-knoe-dev-cluster-agent-0",
"k3d-knoe-dev-cluster-agent-1",
"k3d-knoe-dev-cluster-server-0"
],
"metadata": {
"prior_plan_present": true,
"reason": "reused",
"regenerated": false,
"reused": true
"reason": "assigned_node_no_longer_eligible",
"regenerated": true,
"reused": false
},
"plan_hash": "334860873593911a",
"plan_id": "cnpg-placement-334860873593911a",
"plan_hash": "adaed574a7910245",
"plan_id": "cnpg-placement-adaed574a7910245",
"schema_version": "v1"
}

View File

@ -1,15 +1,15 @@
{
"assignments": {
"0": "gk3-knoe-dev-0-pool-1-11c87d92-8qr5",
"1": "gk3-knoe-dev-0-pool-1-88b7392b-62c4",
"2": "gk3-knoe-dev-0-pool-1-88b7392b-d6lb"
"0": "k3d-knoe-dev-cluster-agent-0",
"1": "k3d-knoe-dev-cluster-agent-1",
"2": "k3d-knoe-dev-cluster-server-0"
},
"cluster_name": "knoe-db",
"desired_instances": 3,
"eligible_nodes": [
"gk3-knoe-dev-0-pool-1-11c87d92-8qr5",
"gk3-knoe-dev-0-pool-1-88b7392b-62c4",
"gk3-knoe-dev-0-pool-1-88b7392b-d6lb"
"k3d-knoe-dev-cluster-agent-0",
"k3d-knoe-dev-cluster-agent-1",
"k3d-knoe-dev-cluster-server-0"
],
"metadata": {
"prior_plan_present": true,
@ -17,7 +17,7 @@
"regenerated": false,
"reused": true
},
"plan_hash": "334860873593911a",
"plan_id": "cnpg-placement-334860873593911a",
"plan_hash": "adaed574a7910245",
"plan_id": "cnpg-placement-adaed574a7910245",
"schema_version": "v1"
}

View File

@ -1,5 +1,5 @@
; Prole Master Configuration File
; Generated by install.py on 2026-04-04 12:25:21
; Generated by install.py on 2026-04-05 01:23:00
; This file is used as input for Ansible deployment and k8s cluster creation.
[User]
@ -56,7 +56,7 @@ 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:ZAG_hQCgEUXMSqra:6ipuCNnUu2hC4n0jmsdt_Z4ILvTuPeRs5GqLNhCnBiRKSSlJmwp9PW4f36hYLr_06iHi63laqwCGXLGFW5KCmqUcNerl_ntmZNS10k6xkj--BEqAf6O2JnHotZoCu8EkNDh-xnyBTflXE3oLTTafUHFjhqZQ_YXYA-resA==}
init_cluster.k3s_token = ${PROLE_SECRET:v1:hlXkN11mhpvAfmgS:R1-wv-Sa29MQp6izxht7qJxqUkZz1zMeWWPWSeIWfms26XkApuWGFzYwZt0Ii2pyJDkYqBd_Yx5JH4VbyK6YCnVUXOa8eqAmhUObK-qr3hek7o0e3zOnLIyEj3nEZa8HMcplf8h1hN1M2CqsQtE-Mjh5zUyRAEEZ18VMag==}
init_cluster.kerberos_enabled = true
init_cluster.mode = k3d
init_cluster.start_cluster = true
@ -80,7 +80,7 @@ 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
supabase_config.pv_base_dir = /synology/d005
supabase_config.pv_node = gandalf.prole.org
@ -89,6 +89,9 @@ supabase_config.pv_node = gandalf.prole.org
; Variables used by name in more than one place or assumed global scope
CLUSTER_ENV = dev
CLUSTER_NAME = knoe-db
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
DATABASE_NAMESPACE = knoe-db-18-008
DB_HOST_PORT = 5432
DEPLOYMENT_MODE = k3d
@ -97,9 +100,9 @@ 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:NMTTkd1v51pfcCXs:elEzbvXUWk_e8Xw_JUwhG2WI9vnyooM03hU3r2nUOPl39gymW2C72TwOSqbsNXWzpyC-pTuEXrDIHVdVQhHpfUQR08kt-qb5Bl_OElRmudqm8jB3JJg8ChWpXAZGd9btbTe9_8LuE4PW9N7z1Ep8u2yNlMz1tL-hcGk0_Q==}
PROLE_K3S_TOKEN = ${PROLE_SECRET:v1:XME8dFpJqpSvNh67:DuFmvxAOsTuoto2bacV28Swp0ohml3GgB954rEofX2zsP_d0RTFYIcReRzAdBTDUS4F8oXcCex7CGR8L-AmaSQkftzVq1VA5r6ljb7Cid6xZw56aQwH6fr4t2ba32uuUEBpBxiXfmoqYZACddSvb1-HR7VXC259YpSW-LA==}
PROLE_OPENTOFU_URL = http://127.0.0.1:8080
SERVICE_NAMESPACE = default
SERVICE_NAMESPACE = knoe-system
SUPABASE_PV_BASE = /synology/d005
SUPABASE_PV_BASE_DIR = /synology/d005
SUPABASE_PV_NODE = gandalf.prole.org
@ -145,7 +148,10 @@ OLLAMA_SERVER_HOST = fairyland.prole.org
OLLAMA_SERVER_PORT = 11434
[Optional Features]
; No configuration values captured yet for this section.
AT_REST_ENCRYPTION_ENABLED = True
GITOPS_ENABLED = False
KERBEROS_ENABLED = True
SUPABASE_ENABLED = False
[GitOps]
; No configuration values captured yet for this section.
@ -154,7 +160,9 @@ OLLAMA_SERVER_PORT = 11434
; No configuration values captured yet for this section.
[Initialize Cluster]
; No configuration values captured yet for this section.
ENVIRONMENT = dev
K3S_SERVER_URL = https://myrddin.prole.org:6443
K3S_TOKEN = ${PROLE_SECRET:v1:Puz4fzkm528-XoYM:YlPecZrTA2orKSD6NeuquMD3J39L4RGXqOzGzgNzHqmrhLPPlpLaczU3CwoM3omEuF6rjd0NVlf7Ief5HSKPm49S-X70x-vH_9QaiY8bPx3xtfypGC--p-05NQIw_rDWz5mdonzzj41U-zxA9N3r5mVILiBAkI1trkPRLg==}
[Dev Cluster (k3d)]
CLUSTER_ENV = dev
@ -165,12 +173,15 @@ 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:THNElHPMqx8EPHxg:2BOEHH50KsNSMXLupemftQOZ8X8NBjnOaWsi50qdZZ-M68KanY-5K1hxOsRJ2P90FWqKGf2H4VqjQWog6Z9aaLkOnT_AW9s3XER1KjUR9rDvNHwO4PKYbesmzYcBI_qwkEPwbxCaWl_ralcKxaQBKP4XoHH6eYm6dDhZYw==}
K3S_TOKEN = ${PROLE_SECRET:v1:IjPqAfeqHKD4cmtX:su6LTjq1oMB6FwCDw53JhUPPgmoLTjiEtobR20AakbOFuIfQLUqz_nrfJTMV17Ip9BMUB6hUc9leZo1qSWnDFY_rlWUOLxaGtNOUBxVNUPzRJtLEmXDJ5T3hJDQZvukPcTvN8m_3gY72MGMgC5zhTvQI3eEli0ZZ17q3tg==}
MODE = k3s
PIPELINE_URL = http://127.0.0.1:8080
[GCP]
; No configuration values captured yet for this section.
BILLING_ACCOUNT = 01193C-25783B-3211AD
BILLING_PROJECT = plenary-truck-485623-p7
ORG_ID = 584001916389
PROJECT_ID = plenary-truck-485623-p7
[Prod Cluster (k8s)]
ARTIFACTS_DIR = /Users/chrisfu/dev/prole/data/staging
@ -180,7 +191,8 @@ MODE = k8s
PIPELINE_URL = http://127.0.0.1:8080
[Docker Build]
; No configuration values captured yet for this section.
LOCAL_REGISTRY = localhost:5000
LOCAL_REGISTRY_INTERNAL = k3d-prole-registry.localhost:5000
[Initialization Scripts]
; No configuration values captured yet for this section.

View File

@ -1,11 +1,3 @@
# Port mappings for Prole Tools (generated).
# Format: key: local=... remote=... ns=... svc=... address=...
argocd: local=8081 remote=80 ns=argocd svc=argocd-server address=0.0.0.0
garage: local=3900 remote=3900 ns=knoe-system svc=garage address=0.0.0.0
openbao: local=8200 remote=8200 ns=knoe-system svc=openbao address=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-0 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

View File

@ -1,5 +1,5 @@
; Prole Master Configuration File
; Generated by install.py on 2026-04-04 12:31:19
; Generated by install.py on 2026-04-05 01:48:40
; This file is used as input for Ansible deployment and k8s cluster creation.
[User]
@ -39,15 +39,15 @@ 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.CLUSTER_NAME = ${CLUSTER_NAME}
env_setup.DATABASE_NAMESPACE = ${DATABASE_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
gitops.git_provider = Gitea
gitops.node_selector =
init_cluster.argocd_enabled = false
@ -56,7 +56,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:lkXqS7JRjESKcQr9:yZTvAEY_BiN_nhPgyhmV9i8bRHXW3Twbi3ZP_gXjrTgBYx8aN0I0wtiZpqW-c21kKFMo8TmQ5EJ5Sd8LZIh7ARuGztax3_9_DIxjHbEN4Ja0EPy5EThncJ6cLXm-ycF6Pwq2sOr62rL7ZSxmvDbPBXm3B22WGRJOh_nF3A==}
init_cluster.k3s_token = ${PROLE_SECRET:v1:00IKKRpdvdWHIF6m:9J5xviuXy_oiUsP4S6nm80PAwLKwqgw5ziEuE3FTrt0Sf6KcWOseXy7SXQt8UMgLP11Ph7Otts3nah6euSNbRKTBIqVDNL24vcHaSSjuTUvU1tYbQffF4AOVXNJ13_5G3jgPW5Uba_X-2fgsG5bghBF-0v_x9K_NaTxcfg==}
init_cluster.kerberos_enabled = true
init_cluster.mode = k8s
init_cluster.start_cluster = true
@ -67,8 +67,8 @@ 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:1TLhOe9B8E1WJsYH:hFxPKUiWLMJmdbw8yGtyqrPU_I8RyFpOI18aMcCcog9rlSkt}
init_password.db_password_confirm = ${PROLE_SECRET:v1:1TLhOe9B8E1WJsYH:hFxPKUiWLMJmdbw8yGtyqrPU_I8RyFpOI18aMcCcog9rlSkt}
init_password.db_password = ${PROLE_SECRET:v1:C1gpQ0W8x66jxqop:F5xTd8kM8BTAqNiGkrHlZ6XQ3Qr503oo7V5Rs2hHURH-YrbY}
init_password.db_password_confirm = ${PROLE_SECRET:v1:C1gpQ0W8x66jxqop:F5xTd8kM8BTAqNiGkrHlZ6XQ3Qr503oo7V5Rs2hHURH-YrbY}
init_password.db_username = root
init_password.generate_ssh_key = true
init_scripts.run_scripts = true
@ -80,32 +80,33 @@ 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
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
ARTIFACT_REGISTRY_AVAILABLE = true
CLUSTER_ENV = prod
CLUSTER_NAME = knoe-db
CNPG_ELIGIBLE_NODES = gk3-knoe-dev-0-pool-1-11c87d92-8qr5,gk3-knoe-dev-0-pool-1-88b7392b-62c4,gk3-knoe-dev-0-pool-1-88b7392b-d6lb
CNPG_PLACEMENT_PLAN_FILE = ${HOME}/dev/prole/conf/cnpg-placement/knoe-system-knoe-db.json
CNPG_PLACEMENT_PLAN_HASH = 334860873593911a
CNPG_PLACEMENT_PLAN_ID = cnpg-placement-334860873593911a
CNPG_STAGE1_NODE = gk3-knoe-dev-0-pool-1-11c87d92-8qr5
CNPG_ELIGIBLE_NODES = k3d-knoe-dev-cluster-agent-0,k3d-knoe-dev-cluster-agent-1,k3d-knoe-dev-cluster-server-0
CNPG_PLACEMENT_PLAN_FILE = /Users/chrisfu/dev/prole/conf/cnpg-placement/knoe-system-knoe-db.json
CNPG_PLACEMENT_PLAN_HASH = adaed574a7910245
CNPG_PLACEMENT_PLAN_ID = cnpg-placement-adaed574a7910245
CNPG_STAGE1_NODE = k3d-knoe-dev-cluster-agent-0
DATABASE_NAMESPACE = knoe-db-0
DB_HOST_PORT = 5432
DB_PASSWORD = ${PROLE_SECRET:v1:bGkdlAlb8osFLRrZ:aR9z9sOfxvVXsafDHDLNopioqwfNlV9lkALaxM-AF8CrzPe5}
DB_PASSWORD = ${PROLE_SECRET:v1:RrW-X0wEk60ytw2L:EgNqfY2Ox__oRWttWsPVJjYy2zo7jNEKugj8u36vvfuecJxJ}
DEPLOYMENT_MODE = k8s
DEPLOYMENT_TARGET = prole-prod-cluster
DOCKER_PRELOAD = false
KNOE_DB_USER = root
KUBECONFIG = ${HOME}/.kube/config
KUBECONTEXT = gke_plenary-truck-485623-p7_us-west3_knoe-dev-0
KUBECONFIG = /Users/chrisfu/.kube/config
KUBECONTEXT = k3d-knoe-dev-cluster
OPTIONAL_WORKLOADS_MIN_READY_SCHEDULABLE_NODES = 2
PROLE_K3S_SERVER = https://myrddin.prole.org:6443
PROLE_K3S_TOKEN = ${PROLE_SECRET:v1:AIY1H6BntV97_h-F:lEi5q_GtOlxpZLHah0aYVObAAeWknsz94-SaXOHD9TaVF_zXaHoHo9L4gwvU67XKnnU6DjGsvIumtvW98ChoBfmHfj3n4OHve_MYokZKSSlr0HfGjUmnaXVJ1YuJAJsG-7XZGa-Itv9zYciQ2-4TM6UtBRPdAzar6kkrwA==}
PROLE_K3S_TOKEN = ${PROLE_SECRET:v1:OnDkamBldOCeAKF8:FN47uqDWbzSfqXzKxEaKEGEx14G8rCintVIBsaN60LNNLR5-4X0-KlFyKG4ZtrsDAy_x7c1CDvJePBoMDmhWmpriAPNEsDz6_K7UakxfSfuJvQzSnKulcBec9YhNewBTd9lauf6JD6vx8Fm7dX8PuS3EaXNJQyGzY1_07w==}
SERVICE_NAMESPACE = knoe-system
SUPABASE_PV_BASE = /synology/d005
SUPABASE_PV_BASE_DIR = /synology/d005
@ -121,8 +122,8 @@ SUPABASE_PV_NODE = gandalf.prole.org
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","gandalf.prole.org"],"pihole":["pi.prole.org","raspberry.prole.org"],"ad_dc":["myrddin.prole.org"],"k3s_servers":["myrddin.prole.org"],"k3s_agents":["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
@ -133,11 +134,11 @@ KERBEROS_AUTO_ENABLED = True
; No configuration values captured yet for this section.
[System Environment]
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.
@ -165,19 +166,19 @@ SUPABASE_ENABLED = False
[Initialize Cluster]
ENVIRONMENT = prod
K3S_SERVER_URL = https://myrddin.prole.org:6443
K3S_TOKEN = ${PROLE_SECRET:v1:5xChfOZ00d7VjXyT:F56d9li8Q_-DkWo7a8Nx14gKx_8NHgfF3vD0MRUJfgdodI1HunR1zCPq1XZFpfJJxVRMuwsvahnCRo-0uvlfjANA6RTvrcyeRxNKQr-QsRRJ1BdnQ0dWuJyuy1lbOAz6SHl079OxQJTibjz1A8rOIDeY-L9wboBabPf0Kg==}
K3S_TOKEN = ${PROLE_SECRET:v1:NFdcfCobgzp-u-Lj:YdTVsoCjyS76VzIoUqUzBHDF2tXPTUTeYZk0RGPK7EF6UNOXG1smsfyzWq-xhEnqGYoRLfV4Ni7AOuGeSwUuwSq4xpVRoF7MSMMF5-pwc-aLPv8U3bmPVcfNyoZ7BJvlwEvir3SQKpQbUggL5B4hWxMSl33k6yW0BfGJyA==}
[Dev Cluster (k3d)]
CLUSTER_ENV = dev
DISPLAY_NAME = knoe-dev-cluster
KUBECTL_CONTEXT = gke_plenary-truck-485623-p7_us-west3_knoe-dev-0
KUBECTL_CONTEXT = k3d-knoe-dev-cluster
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:3w94MClqbC2ygx3c:gzXxHXm5u_Au6o2aN_SS5jhcY9OULo_3QadNpDwURnYSt5TJ0FaaJMAOxQ5A0qP6XxRgboWCvEI4HN90AJnzjMJvplV-9Okljpx8UMoYPaiMqP5omLgkpKf1CsyWX7bvyFM9wuU9wqvfb4qIARTYFPABsRha4N_BvRVFiQ==}
K3S_TOKEN = ${PROLE_SECRET:v1:rUWR4v1suD9K4a1i:44pggdzZCCkkLJ85cpnCOyWf4KGB888i5mrPGp6nKxPT9EDjyC_GXxDmZMFxxkXg9DodYCnMbfk_MyrO6j1tWGgakGBHqeQxoHgKvZKlVXkmvbbst-KWLBSl0ADAKrfYgxsBgybYLpt1S04OmqAdbB7QLt-HDQLQ0G0g2w==}
MODE = k3s
[GCP]
@ -191,7 +192,7 @@ org_id = 584001916389
project_id = plenary-truck-485623-p7
[Prod Cluster (k8s)]
ARTIFACTS_DIR = ${HOME}/dev/prole/data/staging
ARTIFACTS_DIR = /Users/chrisfu/dev/prole/data/staging
CLUSTER_ENV = prole-prod-cluster
DISPLAY_NAME = prole-prod-cluster
INSTALL_PLAN = Validate production config and required secrets
@ -204,7 +205,7 @@ Configure routing, TLS, and frontdoor domain mappings
Prepare migration artifacts from source environment
Generate OpenTofu plan and apply production pipeline
MODE = k8s
OPENTOFU_VARS = {"admin_user":"knoe_admin","app_database":"knoey","app_user":"knoey_app","backup_bucket":"knoe-0-backups","backup_provider":"gcs","backup_retention_days":14,"bootstrap_admin_email":"admin@knoey.com","cluster_name":"knoe-dev-0","cnpg_cluster_name":"knoe-db","db_instances":3,"db_storage_class":"premium-rwo","db_storage_size_gi":100,"ecosystem_id":0,"environment":"production","frontdoor_host":"knoey.com","gcp_project_id":"plenary-truck-485623-p7","gcp_region":"\u2713 us-west3","google_hosted_domain":"","meta_database":"knoe_meta","migration_continuous_until_cutover":false,"migration_mode":"snapshot-restore","migration_source_database":"knoey","migration_source_environment":"prole.org","migration_source_host":"knoe-local-db.prole.org","migration_source_password_ref":"secretref://local-source-db-password","migration_source_port":5432,"migration_source_user":"replication_user","namespace":"ecosystem-0","oidc_client_id_ref":"secretref://google-oidc-client-id","oidc_client_secret_ref":"secretref://google-oidc-client-secret","oidc_issuer":"https://accounts.google.com","platform_domain":"knoe.dev","postgres_version":"18","project_id":"plenary-truck-485623-p7","region":"\u2713 us-west3","tls_mode":"managed","wal_bucket":"knoe-0-wal","workload_identity_sa":"cnpg-backup"}
OPENTOFU_VARS = {"admin_user":"knoe_admin","app_database":"knoey","app_user":"knoey_app","backup_bucket":"knoe-0-backups","backup_provider":"gcs","backup_retention_days":14,"bootstrap_admin_email":"admin@knoey.com","cluster_name":"knoe-dev-0","cnpg_cluster_name":"knoe-db","db_instances":3,"db_storage_class":"premium-rwo","db_storage_size_gi":100,"ecosystem_id":0,"environment":"production","frontdoor_host":"knoey.com","gcp_project_id":"plenary-truck-485623-p7","gcp_region":"\u2713 us-west3","google_hosted_domain":"","meta_database":"knoe_meta","migration_continuous_until_cutover":false,"migration_mode":"snapshot-restore","migration_source_database":"knoey","migration_source_environment":"prole.org","migration_source_host":"knoe-local-db.prole.org","migration_source_password_ref":"secretref://local-source-db-password","migration_source_port":5432,"migration_source_user":"replication_user","namespace":"ecosystem-0","oidc_client_id_ref":"secretref://google-oidc-client-id","oidc_client_secret_ref":"secretref://google-oidc-client-secret","oidc_issuer":"https://accounts.google.com","platform_domain":"knoe.dev","postgres_version":"16","project_id":"plenary-truck-485623-p7","region":"\u2713 us-west3","tls_mode":"managed","wal_bucket":"knoe-0-wal","workload_identity_sa":"cnpg-backup"}
PRODUCTION_CONFIG_YAML = kind: KnoeProductionConfig
metadata:
ecosystemId: 0
@ -225,7 +226,7 @@ spec:
namespace: ecosystem-0
database:
clusterName: knoe-db
postgresVersion: '18'
postgresVersion: '16'
instances: 3
storageClass: premium-rwo
storageSizeGi: 100
@ -269,8 +270,8 @@ spec:
STATUS = Attempted
[Deployment]
MODE = k8s
TARGET = prole-prod-cluster
MODE = k3d
TARGET = prole-dev-cluster
[Install]
; No configuration values captured yet for this section.

View File

@ -5,7 +5,7 @@ apiVersion: v1
kind: ServiceAccount
metadata:
name: cnpg-backup-sa
namespace: ecosystem-0
namespace: knoe-db-0
annotations:
# Set by etc/init_cnpg_gke.sh — substituted at deploy time
iam.gke.io/gcp-service-account: "cnpg-backup@${GCP_PROJECT_ID}.iam.gserviceaccount.com"
@ -17,7 +17,7 @@ apiVersion: postgresql.cnpg.io/v1
kind: ScheduledBackup
metadata:
name: knoe-db-daily
namespace: ecosystem-0
namespace: knoe-db-0
spec:
schedule: "0 2 * * *"
backupOwnerReference: self

View File

@ -2,7 +2,7 @@ apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata:
name: knoe-db
namespace: ecosystem-0
namespace: knoe-db-0
spec:
instances: 3
# Image pulled from GCP Artifact Registry — set ARTIFACT_REGISTRY in conf/prod/gcp.cfg
@ -14,12 +14,20 @@ spec:
affinity:
enablePodAntiAffinity: true
podAntiAffinityType: required # hard: refuse to co-locate pods on the same node
podAntiAffinityType: required # hard: one CNPG pod (rw/ro/r) per node; Pending pods trigger GKE cluster autoscaler
topologyKey: kubernetes.io/hostname # physical node boundary (not zone)
tolerations:
# Allow scheduling on GKE Spot nodes (common in knoe-dev-0 to reduce cost).
# Without this toleration the cluster-autoscaler predicate simulation fails
# for any MIG whose nodes carry the spot taint, blocking scale-up entirely.
- key: "cloud.google.com/gke-spot"
operator: "Equal"
value: "true"
effect: "NoSchedule"
postgresql:
parameters:
shared_buffers: 256MB
shared_buffers: 128MB
pg_stat_statements.max: '10000'
pg_stat_statements.track: all
shared_preload_libraries:
@ -63,6 +71,7 @@ spec:
- GRANT anon TO authenticator;
# demo schema for guest read-only access (evolves over time)
- CREATE SCHEMA IF NOT EXISTS demo;
- CREATE ROLE IF NOT EXISTS guest NOLOGIN;
- GRANT USAGE ON SCHEMA demo TO guest;
- ALTER DEFAULT PRIVILEGES IN SCHEMA demo GRANT SELECT ON TABLES TO guest;
@ -88,6 +97,14 @@ spec:
serverTLSSecret: knoe-db-tls
serverCASecret: knoe-db-ca
resources:
requests:
cpu: "500m"
memory: "512Mi"
limits:
cpu: "2"
memory: "2Gi"
enableSuperuserAccess: true
# GCS backup via Workload Identity — see knoe-db-backup-gcs.yaml
@ -95,8 +112,7 @@ spec:
barmanObjectStore:
destinationPath: "gs://knoe-0-backups/"
googleCredentials:
applicationCredentials:
name: "" # empty = Workload Identity; no static key needed
gkeEnvironment: true # use Workload Identity; no applicationCredentials secret needed
wal:
compression: gzip
data:

View File

@ -1,6 +1,6 @@
apiVersion: v1
kind: Namespace
metadata:
name: ecosystem-0
name: knoe-db-0
labels:
app.kubernetes.io/managed-by: prole-installer

View File

@ -18,9 +18,9 @@
# gcloud iam service-accounts add-iam-policy-binding \
# cnpg-backup@$GCP_PROJECT_ID.iam.gserviceaccount.com \
# --role=roles/iam.workloadIdentityUser \
# --member="serviceAccount:$GCP_PROJECT_ID.svc.id.goog[ecosystem-0/cnpg-backup-sa]"
# --member="serviceAccount:$GCP_PROJECT_ID.svc.id.goog[knoe-db-0/cnpg-backup-sa]"
#
# kubectl annotate serviceaccount cnpg-backup-sa \
# -n ecosystem-0 \
# -n knoe-db-0 \
# iam.gke.io/gcp-service-account=cnpg-backup@$GCP_PROJECT_ID.iam.gserviceaccount.com \
# --overwrite

View File

@ -743,10 +743,12 @@ def install_barman_plugin(
if is_cert_issue:
if not _cert_manager_recovered:
_log(log, "WARN: cert-manager webhook error; ensuring cert-manager and retrying...")
_ensure_cert_manager_for_barman(env=env, log=log)
ca_ready = _ensure_cert_manager_for_barman(env=env, log=log)
_cert_manager_recovered = True
_log(log, "cert-manager recovered; waiting 30 s for webhook CA bundle to propagate...")
time.sleep(30)
if ca_ready:
_log(log, "cert-manager recovered; webhook CA bundle ready.")
else:
_log(log, "WARN: cert-manager restarted but CA bundle not confirmed; proceeding with retries...")
else:
wait_s = min(20 * _attempt, 60)
_log(log, f"Webhook CA not yet trusted (attempt {_attempt}); retrying in {wait_s} s...")
@ -769,6 +771,11 @@ def install_barman_plugin(
env=env, timeout=timeout_s + 10,
)
# Wait for cert-manager to issue the Barman TLS secrets that the pod mounts
tls_timeout = int((env or {}).get("BARMAN_TLS_TIMEOUT", "300") or "300")
if not _wait_for_barman_tls_secrets(env=env, log=log, timeout=tls_timeout):
_log(log, "WARN: Barman Cloud TLS secrets not ready after install; pods may fail to start.")
def _ensure_cert_manager_for_barman(
env: dict | None = None,
@ -851,6 +858,130 @@ def _ensure_cert_manager_for_barman(
timeout=250,
)
_log(log, "Waiting for cert-manager webhook CA bundle to be injected...")
if _wait_for_webhook_ca_bundle(env=env, log=log, timeout=300):
return True
_log(log, "WARN: cert-manager webhook CA bundle not detected within 300 s; attempting manual CA injection...")
if _try_inject_ca_bundle_manually(env=env, log=log):
_log(log, "Manual CA bundle injection succeeded.")
return True
_log(log, "WARN: Manual CA bundle injection also failed; cert-manager webhook may not be fully trusted.")
return False
def _try_inject_ca_bundle_manually(
env: dict | None = None,
log: _LogFn | None = None,
) -> bool:
"""Manually patch the caBundle into the cert-manager ValidatingWebhookConfiguration.
Reads the CA certificate from the ``cert-manager-webhook-ca`` secret (created
by cert-manager itself) and patches it directly into the webhook configuration
when the cainjector is too slow or has not yet reconciled.
Returns True if the patch succeeded and the caBundle is now non-empty.
"""
for secret_name in ("cert-manager-webhook-ca", "cert-manager-cainjector-leader-election"):
r = subprocess.run(
[
"kubectl", "-n", "cert-manager", "get", "secret", secret_name,
"-o", "jsonpath={.data.tls\\.crt}",
],
env=env, capture_output=True, text=True, timeout=15,
)
ca_bundle = (r.stdout or "").strip()
if r.returncode == 0 and ca_bundle:
break
else:
_log(log, "WARN: cert-manager CA secret not found; skipping manual caBundle patch.")
return False
_log(log, "Patching cert-manager-webhook ValidatingWebhookConfiguration with CA bundle...")
patch_json = json.dumps(
[{"op": "replace", "path": "/webhooks/0/clientConfig/caBundle", "value": ca_bundle}]
)
r2 = subprocess.run(
[
"kubectl", "patch", "validatingwebhookconfiguration", "cert-manager-webhook",
"--type=json", f"-p={patch_json}",
],
env=env, capture_output=True, text=True, timeout=20,
)
if r2.returncode != 0:
_log(log, f"WARN: Failed to patch caBundle: {(r2.stderr or r2.stdout or '').strip()}")
return False
# Brief wait for API server to propagate the patch
time.sleep(3)
return _wait_for_webhook_ca_bundle(env=env, log=log, timeout=30)
def _wait_for_webhook_ca_bundle(
env: dict | None = None,
log: _LogFn | None = None,
timeout: int = 300,
) -> bool:
"""Poll until the cert-manager ValidatingWebhookConfiguration has a CA bundle injected.
Returns True if the caBundle field becomes non-empty within *timeout* seconds,
False otherwise.
"""
deadline = time.time() + timeout
interval = 5
while time.time() < deadline:
r = subprocess.run(
[
"kubectl", "get", "validatingwebhookconfiguration",
"cert-manager-webhook",
"-o", "jsonpath={.webhooks[0].clientConfig.caBundle}",
],
env=env, capture_output=True, text=True, timeout=15,
)
if r.returncode == 0 and r.stdout.strip():
return True
time.sleep(interval)
return False
def _wait_for_barman_tls_secrets(
env: dict | None = None,
log: _LogFn | None = None,
timeout: int = 180,
) -> bool:
"""Poll until cert-manager has issued both Barman Cloud TLS secrets in cnpg-system.
The Barman Cloud plugin manifest declares two Certificate resources
(``barman-cloud-client`` and ``barman-cloud-server``). cert-manager
populates the corresponding secrets asynchronously; the pod cannot start
until both secrets exist and have non-empty ``tls.crt`` / ``tls.key`` data.
Returns True when all four fields are present, False on timeout.
"""
_log(log, f"Waiting for Barman Cloud TLS secrets in cnpg-system (timeout: {timeout}s)...")
deadline = time.time() + timeout
interval = 5
while time.time() < deadline:
all_ready = True
for secret in ("barman-cloud-client-tls", "barman-cloud-server-tls"):
for field in ("tls.crt", "tls.key"):
r = subprocess.run(
[
"kubectl", "-n", "cnpg-system", "get", "secret", secret,
"-o", f"jsonpath={{.data.{field.replace('.', '\\.')}}}",
],
env=env, capture_output=True, text=True, timeout=15,
)
if r.returncode != 0 or not (r.stdout or "").strip():
all_ready = False
break
if not all_ready:
break
if all_ready:
_log(log, "Barman Cloud TLS secrets are available.")
return True
time.sleep(interval)
_log(log, f"WARN: Barman Cloud TLS secrets not ready after {timeout}s.")
return False
def _resolve_barman_plugin_tag(env: dict | None, log: _LogFn | None) -> str:
"""Fetch the latest Barman Cloud plugin release tag from GitHub, with fallback."""
@ -887,7 +1018,9 @@ def _bootstrap_db_user_secrets(
_log(log, "WARN: DB_PASSWORD not resolved in env; skipping db-user secret bootstrap.")
return
db_user = ((env or {}).get("KNOE_DB_USER") or "postgres").strip()
# CNPG requires the secret username to match the cluster owner field (owner: prole).
# KNOE_DB_USER is the admin/superuser login name, not the cluster owner.
db_owner = ((env or {}).get("CNPG_DB_OWNER") or "prole").strip()
def _apply_if_missing(secret_name: str, username: str, password: str) -> None:
if _kubectl_ok(["-n", namespace, "get", "secret", secret_name], env=env):
@ -913,7 +1046,7 @@ def _bootstrap_db_user_secrets(
raise RuntimeError(f"Failed to apply secret '{secret_name}':\n{apply_res.stderr}")
_log(log, f"Created secret '{secret_name}' in '{namespace}'.")
_apply_if_missing("knoe-db-user", db_user, pw)
_apply_if_missing("knoe-db-user", db_owner, pw)
_apply_if_missing("knoe-db-superuser", "postgres", pw)

View File

@ -1 +1 @@
8
9

View File

@ -1 +1 @@
13
22

View File

@ -1,56 +1,52 @@
Network Discovery Summary:
Primary Router: 10.0.0.1 (eero_5d:50:f2)
DNS Servers: 10.0.0.5, 100.100.100.100, 10.0.0.4
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.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.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.1 [9c:57:bc:5d:50:f2] (eero_5d:50:f2): Ports [53], Services: ['DNS']
- 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.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.7 [2c:cf:67:ca:93:ce] (RaspberryPi_ca:93:ce): Ports [22], Services: ['SSH']
- 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.7 [2c:cf:67:ca:93:ce] (RaspberryPi_ca:93:ce): Ports [22], Services: ['SSH']
- 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.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.32 [48:a6:b8:a4:8e:cc] (Sonos_a4:8e:cc): Ports [], Services: []
- 10.0.0.2 [dc:a4:ca:ea:1a:2f] (Apple_ea:1a:2f): Ports [], Services: []
- 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.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.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.33 [ec:b5:fa:b0:76:e4] (PhilipsLight_b0:76:e4): Ports [80, 443], 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.37 [54:07:7d:22:c0:b2] (Netgear_22:c0:b2): Ports [80], Services: ['Web Server']
- 10.0.0.44 [24:fc:e5:51:cf:74] (SamsungElect_51:cf:74): Ports [], Services: []
- 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.37 [54:07:7d:22:c0:b2] (Netgear_22:c0:b2): Ports [80], Services: ['Web Server']
- 10.0.0.52 [2c:cf:67:ca:93:ce] (RaspberryPi_ca:93:ce): Ports [22], Services: ['SSH']
- 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.188 [00:16:6c:c5:1f:54] (SamsungElect_c5:1f:54): Ports [80, 443], Services: ['Web Server']
- 10.0.0.73 [4c:a9:19:b3:12:f8] (TuyaSmart_b3:12:f8): Ports [], Services: []
- 10.0.0.206 [00:a0:de:a2:0b:ef] (Yamaha_a2:0b:ef): Ports [80], Services: ['Web Server']
- 10.0.0.35\ [7e:19:9a:fe:9f:ed] (7e:19:9a:fe:9f:ed): 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.106 [48:a6:b8:a7:50:60] (Sonos_a7:50:60): Ports [], Services: []
- 10.0.0.113 [60:81:10:92:02:4e] (Apple_92:02:4e): Ports [], Services: []
- 10.0.0.111 [ca:0b:6c:5d:0a:e5] (ca:0b:6c:5d:0a:e5): Ports [], Services: []
- 10.0.0.170 [b0:ee:7b:ca:73:99] (Roku_ca:73:99): Ports [], Services: []
- 10.0.0.206 [00:a0:de:a2:0b:ef] (Yamaha_a2:0b:ef): Ports [80], Services: ['Web Server']
- 10.0.0.73 [4c:a9:19:b3:12:f8] (TuyaSmart_b3:12:f8): 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.107 [b8:06:0d:b7:7c:56] (TuyaSmart_b7:7c:56): Ports [], Services: []
- 10.0.0.99 [20:28:bc:f1:dc:ba] (Visionscape_f1:dc:ba): 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.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.127 [58:55:ca:4a:60:6e] (Apple_4a:60:6e): Ports [22], Services: ['SSH']
- 10.0.0.130 [f8:bb:bf:a3:84:eb] (eero_a3:84:eb): Ports [53], Services: ['DNS']
- 10.0.0.128 [20:c9:d0:94:30:dd] (Apple_94:30:dd): Ports [5900], Services: ['VNC']
- 10.0.0.143 [60:5f:8d:7a:aa:32] (eero_7a:aa:32): Ports [53], Services: ['DNS']
- 10.0.0.207 [e4:5f:01:90:35:0b] (RaspberryPiT_90:35:0b): Ports [22, 5900], Services: ['SSH', 'VNC']
- 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.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.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.155 [62:17:97:b3:06:a1] (62:17:97:b3:06:a1): Ports [], Services: []
- 10.0.0.128 [20:c9:d0:94:30:dd] (Apple_94:30:dd): Ports [], Services: []
- 10.0.0.208 [a0:ad:9f:30:84:5f] (ASUSTekCOMPU_30:84:5f): Ports [], Services: []
- 10.0.0.170 [b0:ee:7b:ca:73:99] (Roku_ca:73:99): Ports [], Services: []
- 10.0.0.155 [62:17:97:b3:06:a1] (62:17:97:b3:06:a1): 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.175 [28:80:88:e4:10:1a] (Netgear_e4:10:1a): Ports [80], Services: ['Web Server']
- 10.0.0.196 [60:5f:8d:88:08:f2] (eero_88:08:f2): Ports [53], Services: ['DNS']
- 10.0.0.194 [64:16:66:ac:04:a3] (NestLabs_ac:04:a3): Ports [], Services: []
- 10.0.0.199 [7c:a6:b0:01:a7:33] (7c:a6:b0:01:a7:33): Ports [], Services: []
- 10.0.0.204 [50:eb:f6:56:f3:26] (ASUSTekCOMPU_56:f3:26): Ports [22, 3389, 445, 5900, 11434], Services: ['SSH', 'VNC', 'SMB/CIFS (Possible Windows/AD)', 'RDP (Windows)', 'Ollama']
- 10.0.0.1\ [9c:57:bc:5d:50:f2] (eero_5d:50:f2): Ports [], Services: []
Ollama Instances found at: 10.0.0.35, 10.0.0.204
Ansible Inventory Summary:

View File

@ -0,0 +1,302 @@
from __future__ import annotations
from types import SimpleNamespace
from unittest.mock import patch, call
import pytest
from knoe.core.ops import cloudnative_pg
# ---------------------------------------------------------------------------
# _wait_for_webhook_ca_bundle
# ---------------------------------------------------------------------------
def test_wait_for_webhook_ca_bundle_returns_true_when_ca_bundle_present(monkeypatch):
"""Returns True immediately when caBundle is non-empty on the first poll."""
calls = []
def _fake_run(args, **kwargs):
calls.append(args)
return SimpleNamespace(returncode=0, stdout="dGVzdA==", stderr="")
monkeypatch.setattr(cloudnative_pg.subprocess, "run", _fake_run)
monkeypatch.setattr(cloudnative_pg.time, "sleep", lambda _: None)
result = cloudnative_pg._wait_for_webhook_ca_bundle(env=None, log=None, timeout=30)
assert result is True
assert len(calls) == 1
assert "cert-manager-webhook" in calls[0]
assert "jsonpath={.webhooks[0].clientConfig.caBundle}" in calls[0]
def test_wait_for_webhook_ca_bundle_returns_false_on_timeout(monkeypatch):
"""Returns False when caBundle never appears within the timeout."""
monkeypatch.setattr(
cloudnative_pg.subprocess, "run",
lambda *_a, **_kw: SimpleNamespace(returncode=0, stdout="", stderr=""),
)
monkeypatch.setattr(cloudnative_pg.time, "sleep", lambda _: None)
# Patch time.time to advance past the deadline quickly
_times = iter([0.0, 1.0, 2.0, 200.0]) # deadline=5; last value exceeds it
monkeypatch.setattr(cloudnative_pg.time, "time", lambda: next(_times))
result = cloudnative_pg._wait_for_webhook_ca_bundle(env=None, log=None, timeout=5)
assert result is False
def test_wait_for_webhook_ca_bundle_returns_true_after_retries(monkeypatch):
"""Returns True once caBundle becomes non-empty after initial empty responses."""
responses = [
SimpleNamespace(returncode=0, stdout="", stderr=""),
SimpleNamespace(returncode=1, stdout="", stderr="not found"),
SimpleNamespace(returncode=0, stdout="dGVzdA==", stderr=""),
]
_iter = iter(responses)
monkeypatch.setattr(cloudnative_pg.subprocess, "run", lambda *_a, **_kw: next(_iter))
monkeypatch.setattr(cloudnative_pg.time, "sleep", lambda _: None)
result = cloudnative_pg._wait_for_webhook_ca_bundle(env=None, log=None, timeout=60)
assert result is True
# ---------------------------------------------------------------------------
# _try_inject_ca_bundle_manually
# ---------------------------------------------------------------------------
def test_try_inject_ca_bundle_manually_succeeds(monkeypatch):
"""Patches the webhook caBundle from the cert-manager-webhook-ca secret."""
logs: list[str] = []
run_calls: list[list] = []
def _fake_run(args, **kwargs):
run_calls.append(list(args))
# Secret fetch returns a valid CA bundle
if "get" in args and "secret" in args:
return SimpleNamespace(returncode=0, stdout="dGVzdA==", stderr="")
# Patch command succeeds
if "patch" in args and "validatingwebhookconfiguration" in args:
return SimpleNamespace(returncode=0, stdout="patched", stderr="")
# Final poll: caBundle is now present
if "jsonpath={.webhooks[0].clientConfig.caBundle}" in args:
return SimpleNamespace(returncode=0, stdout="dGVzdA==", stderr="")
return SimpleNamespace(returncode=0, stdout="", stderr="")
monkeypatch.setattr(cloudnative_pg.subprocess, "run", _fake_run)
monkeypatch.setattr(cloudnative_pg.time, "sleep", lambda _: None)
result = cloudnative_pg._try_inject_ca_bundle_manually(env=None, log=logs.append)
assert result is True
assert any("Patching cert-manager-webhook" in m for m in logs)
def test_try_inject_ca_bundle_manually_returns_false_when_secret_missing(monkeypatch):
"""Returns False and logs a warning when neither CA secret is found."""
logs: list[str] = []
monkeypatch.setattr(
cloudnative_pg.subprocess, "run",
lambda *_a, **_kw: SimpleNamespace(returncode=1, stdout="", stderr="not found"),
)
monkeypatch.setattr(cloudnative_pg.time, "sleep", lambda _: None)
result = cloudnative_pg._try_inject_ca_bundle_manually(env=None, log=logs.append)
assert result is False
assert any("CA secret not found" in m for m in logs)
def test_try_inject_ca_bundle_manually_returns_false_when_patch_fails(monkeypatch):
"""Returns False when the kubectl patch command fails."""
logs: list[str] = []
def _fake_run(args, **kwargs):
if "get" in args and "secret" in args:
return SimpleNamespace(returncode=0, stdout="dGVzdA==", stderr="")
if "patch" in args:
return SimpleNamespace(returncode=1, stdout="", stderr="patch failed")
return SimpleNamespace(returncode=0, stdout="", stderr="")
monkeypatch.setattr(cloudnative_pg.subprocess, "run", _fake_run)
monkeypatch.setattr(cloudnative_pg.time, "sleep", lambda _: None)
result = cloudnative_pg._try_inject_ca_bundle_manually(env=None, log=logs.append)
assert result is False
assert any("Failed to patch caBundle" in m for m in logs)
# ---------------------------------------------------------------------------
# install_barman_plugin — cert-manager recovery path
# ---------------------------------------------------------------------------
def test_install_barman_plugin_succeeds_after_cert_manager_recovery(monkeypatch):
"""Plugin apply succeeds on the second attempt after cert-manager recovery."""
logs: list[str] = []
_apply_calls = [0]
_cert_mgr_called = [False]
def _fake_run(args, **kwargs):
if args[:2] == ["kubectl", "apply"]:
_apply_calls[0] += 1
if _apply_calls[0] == 1:
return SimpleNamespace(
returncode=1,
stdout="",
stderr="webhook.cert-manager.io: failed to call webhook: tls: failed to verify certificate: x509: certificate signed by unknown authority",
)
return SimpleNamespace(returncode=0, stdout="configured", stderr="")
# CA bundle poll
if "jsonpath={.webhooks[0].clientConfig.caBundle}" in args:
return SimpleNamespace(returncode=0, stdout="dGVzdA==", stderr="")
return SimpleNamespace(returncode=0, stdout="", stderr="")
monkeypatch.setattr(cloudnative_pg.subprocess, "run", _fake_run)
monkeypatch.setattr(cloudnative_pg.time, "sleep", lambda _: None)
def _fake_ensure(**_kw):
_cert_mgr_called[0] = True
return True # CA bundle confirmed ready
monkeypatch.setattr(cloudnative_pg, "_ensure_cert_manager_for_barman", _fake_ensure)
monkeypatch.setattr(cloudnative_pg, "_kubectl", lambda *_a, **_kw: SimpleNamespace(returncode=0, stdout="", stderr=""))
monkeypatch.setattr(cloudnative_pg, "_wait_for_barman_tls_secrets", lambda **_kw: True)
env = {"BARMAN_PLUGIN_MANIFEST_URL": "https://example.com/barman.yaml"}
cloudnative_pg.install_barman_plugin(env=env, log=logs.append)
assert _cert_mgr_called[0], "cert-manager recovery must be triggered"
assert any("cert-manager recovered; webhook CA bundle ready" in m for m in logs)
assert _apply_calls[0] == 2
def test_install_barman_plugin_logs_warn_when_ca_not_confirmed(monkeypatch):
"""Logs a warning (not 'ready') when _ensure_cert_manager_for_barman returns False."""
logs: list[str] = []
_apply_calls = [0]
def _fake_run(args, **kwargs):
if args[:2] == ["kubectl", "apply"]:
_apply_calls[0] += 1
if _apply_calls[0] == 1:
return SimpleNamespace(
returncode=1, stdout="", stderr="x509: certificate signed by unknown authority",
)
return SimpleNamespace(returncode=0, stdout="ok", stderr="")
return SimpleNamespace(returncode=0, stdout="", stderr="")
monkeypatch.setattr(cloudnative_pg.subprocess, "run", _fake_run)
monkeypatch.setattr(cloudnative_pg.time, "sleep", lambda _: None)
monkeypatch.setattr(cloudnative_pg, "_ensure_cert_manager_for_barman", lambda **_kw: False)
monkeypatch.setattr(cloudnative_pg, "_kubectl", lambda *_a, **_kw: SimpleNamespace(returncode=0, stdout="", stderr=""))
monkeypatch.setattr(cloudnative_pg, "_wait_for_barman_tls_secrets", lambda **_kw: True)
env = {"BARMAN_PLUGIN_MANIFEST_URL": "https://example.com/barman.yaml"}
cloudnative_pg.install_barman_plugin(env=env, log=logs.append)
assert any("CA bundle not confirmed" in m for m in logs)
assert not any("webhook CA bundle ready" in m for m in logs)
def test_install_barman_plugin_raises_after_all_retries_exhausted(monkeypatch):
"""RuntimeError is raised if all 6 attempts fail with webhook CA errors."""
monkeypatch.setattr(
cloudnative_pg.subprocess, "run",
lambda *_a, **_kw: SimpleNamespace(
returncode=1,
stdout="",
stderr="x509: certificate signed by unknown authority",
),
)
monkeypatch.setattr(cloudnative_pg.time, "sleep", lambda _: None)
monkeypatch.setattr(
cloudnative_pg, "_ensure_cert_manager_for_barman",
lambda **_kw: False,
)
monkeypatch.setattr(cloudnative_pg, "_kubectl", lambda *_a, **_kw: SimpleNamespace(returncode=0, stdout="", stderr=""))
env = {"BARMAN_PLUGIN_MANIFEST_URL": "https://example.com/barman.yaml"}
with pytest.raises(RuntimeError, match="Failed to apply Barman Cloud plugin after cert-manager restart"):
cloudnative_pg.install_barman_plugin(env=env, log=None)
# ---------------------------------------------------------------------------
# _wait_for_barman_tls_secrets
# ---------------------------------------------------------------------------
def test_wait_for_barman_tls_secrets_returns_true_when_secrets_present(monkeypatch):
"""Returns True immediately when both TLS secrets have all fields populated."""
logs: list[str] = []
monkeypatch.setattr(
cloudnative_pg.subprocess, "run",
lambda *_a, **_kw: SimpleNamespace(returncode=0, stdout="dGVzdA==", stderr=""),
)
monkeypatch.setattr(cloudnative_pg.time, "sleep", lambda _: None)
result = cloudnative_pg._wait_for_barman_tls_secrets(env=None, log=logs.append, timeout=30)
assert result is True
assert any("available" in m for m in logs)
def test_wait_for_barman_tls_secrets_returns_false_on_timeout(monkeypatch):
"""Returns False when secrets never appear within the timeout."""
logs: list[str] = []
monkeypatch.setattr(
cloudnative_pg.subprocess, "run",
lambda *_a, **_kw: SimpleNamespace(returncode=1, stdout="", stderr="not found"),
)
monkeypatch.setattr(cloudnative_pg.time, "sleep", lambda _: None)
_times = iter([0.0, 1.0, 2.0, 200.0])
monkeypatch.setattr(cloudnative_pg.time, "time", lambda: next(_times))
result = cloudnative_pg._wait_for_barman_tls_secrets(env=None, log=logs.append, timeout=5)
assert result is False
assert any("not ready" in m for m in logs)
def test_wait_for_barman_tls_secrets_returns_true_after_retry(monkeypatch):
"""Returns True once secrets become available after an initial miss."""
logs: list[str] = []
_call_count = [0]
def _fake_run(args, **kwargs):
_call_count[0] += 1
# First 4 calls (one full secret loop) return empty; after that return populated
if _call_count[0] <= 4:
return SimpleNamespace(returncode=0, stdout="", stderr="")
return SimpleNamespace(returncode=0, stdout="dGVzdA==", stderr="")
monkeypatch.setattr(cloudnative_pg.subprocess, "run", _fake_run)
monkeypatch.setattr(cloudnative_pg.time, "sleep", lambda _: None)
result = cloudnative_pg._wait_for_barman_tls_secrets(env=None, log=logs.append, timeout=60)
assert result is True
def test_install_barman_plugin_warns_when_tls_secrets_not_ready(monkeypatch):
"""Logs a warning when _wait_for_barman_tls_secrets returns False after successful apply."""
logs: list[str] = []
monkeypatch.setattr(
cloudnative_pg.subprocess, "run",
lambda *_a, **_kw: SimpleNamespace(returncode=0, stdout="configured", stderr=""),
)
monkeypatch.setattr(cloudnative_pg.time, "sleep", lambda _: None)
monkeypatch.setattr(cloudnative_pg, "_kubectl", lambda *_a, **_kw: SimpleNamespace(returncode=0, stdout="", stderr=""))
monkeypatch.setattr(cloudnative_pg, "_wait_for_barman_tls_secrets", lambda **_kw: False)
env = {"BARMAN_PLUGIN_MANIFEST_URL": "https://example.com/barman.yaml"}
cloudnative_pg.install_barman_plugin(env=env, log=logs.append)
assert any("pods may fail to start" in m for m in logs)