Remove prole-db-manager; simplify deployment via prole-authority; fix pg18 downgrade & cluster name

Summary:
Removed the prole-db-manager microservice and simplified deployment to use
prole-authority as the internal management and authorization point. Fixed two
blocking bugs that prevented silent install from completing on knoe-dev-cluster.

Removed: prole-db-manager
- Deleted db-manager-deployment.yaml and db-manager-service.yaml from opentofu manifests
- Deleted src/db-manager/ (Dockerfile, server.js, package.json, tests)
- Removed prole-db-manager port-forward mapping from installer/core/env.py
- Removed init_db_manager.sh from Initialization Scripts (milestones.py, actions.py)
- Removed init_certmgr.sh and init_db_manager.sh tabs from services screen (services.py)
- Removed live k8s Deployment/Service from knoe-dev-cluster

Fixed: PostgreSQL version downgrade error (pg17 -> pg18)
- Created conf/postgresql/.version with value 18
- Updated k8s/prole/prole-db.yaml and prole-db-recovery.yaml.tpl imageName to prole-db:18-089
- Fixed _init_database_options_state() to restore saved version_type from prole.cfg
  so db_version_type defaults to v18 (pg18) instead of silently reverting to pg17
- Added database_options.* keys to _collect_input_snapshot() in cfg.py so
  distribution, version_type, and all extension toggles persist to prole.cfg

Fixed: Cluster name inconsistency
- Removed stale prole-dev-cluster references; all scripts now use knoe-dev-cluster
- Added knoe-dev-cluster to mode-detection case in etc/prole_cfg.sh

Config: conf/prole.cfg
- Set kerberos_config.enabled = False, KERBEROS_AUTO_ENABLED = False
- Added database_options.distribution = percona, version_type = v18
- Added all 13 extension flags set to True (postgis, pgvector, pgcrypto, pgaudit,
  pg_repack, pg_stat_statements, pg_buffercache, pg_freespacemap, pgrowlocks,
  postgres_fdw, dblink, pg_stat_monitor, pgbadger)

Verification:
./install.py -s -l -v -c conf/prole.cfg completed successfully.
CNPG deployed prole-db:18-089 to knoe-dev-cluster; all milestones passed.

Co-authored-by: Junie <junie@jetbrains.com>
This commit is contained in:
chrisfu 2026-03-01 20:40:44 -08:00
parent 46f31843c2
commit 5618b662dd
83 changed files with 4721 additions and 3797 deletions

View File

@ -1,199 +0,0 @@
; Prole Master Configuration File
; Generated by install.py on 2026-02-26 00:56:29
; This file is used as input for Ansible deployment and k8s cluster creation.
[User]
; User-editable values; derived values below reference these by default.
NAMESPACE = ${NAMESPACE}
PROLE_CONF = ${PROLE_HOME}/conf
PROLE_DATA = /opt/prole/data/chrisfu
PROLE_HOME = /Users/chrisfu/dev/prole
PROLE_LOGS = /opt/prole/logs/chrisfu
PROLE_SERVICE = ${PROLE_HOME}/etc
SERVICE_NAMESPACE = default
[Inputs]
; Screen-scoped inputs used for unattended replays (-S)
build.deploy_env = Dev
build.run_build = false
dependencies.ansible.install = true
dependencies.auto_install_missing = true
dependencies.brew.install = true
dependencies.docker.install = true
dependencies.k3d.install = true
dependencies.kubectl.install = true
dependencies.opentofu.install = true
dependencies.python.install = true
dependencies.verify_all = false
disk_selection.disk_type = local
disk_selection.local_path = ${PROLE_HOME}/prole-tools-app/dist
disk_selection.removable_mount =
env_setup.NAMESPACE = ${NAMESPACE}
env_setup.PROLE_CONF = ${PROLE_CONF}
env_setup.PROLE_DATA = ${PROLE_DATA}
env_setup.PROLE_HOME = ${PROLE_HOME}
env_setup.PROLE_LOGS = ${PROLE_LOGS}
env_setup.PROLE_SERVICE = ${PROLE_SERVICE}
init_cluster.at_rest_encryption_enabled = true
init_cluster.cluster_env = k3d-prole-dev-cluster
init_cluster.gitops_enabled = false
init_cluster.k3s_server_url = https://pi.prole.org:6443
init_cluster.k3s_token = ${OPENBAO:kv/prole/${NAMESPACE}/k3s#token}
init_cluster.kerberos_enabled = false
init_cluster.start_cluster = true
init_cluster.supabase_enabled = true
init_cnpg_deploy.force_rollout = false
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 = ${OPENBAO:kv/prole/${NAMESPACE}/db#password}
init_password.db_password_confirm = ${OPENBAO:kv/prole/${NAMESPACE}/db#password}
init_password.db_username = chrisfu
init_password.generate_ssh_key = true
init_scripts.run_scripts = true
kerberos_config.enabled = false
kerberos_config.init_authority = false
kerberos_config.kdc = 10.0.0.3
kerberos_config.password = ${OPENBAO:kv/prole/${NAMESPACE}/kerberos#password}
kerberos_config.realm = PROLE.ORG
kerberos_config.test_connection = false
kerberos_config.user =
network_scan.run = true
ollama_config.model =
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 = k3d-prole-dev-cluster
DB_HOST_PORT = 5432
DB_PASSWORD = ${OPENBAO:kv/prole/${NAMESPACE}/db#password}
DEPLOYMENT_MODE = k3d
DEPLOYMENT_TARGET = prole-dev-cluster
DOCKER_IMPORT_DIR =
NAMESPACE = ${NAMESPACE}
PROLE_DB_USER = chrisfu
PROLE_ENABLE_LOCAL_REGISTRY = true
PROLE_HOME = ${PROLE_HOME}
PROLE_K3S_SERVER = https://pi.prole.org:6443
PROLE_K3S_TOKEN = ${PROLE_SECRET:v1:7PxbzYh-J2MWFvEr:4HQYe1HI1OOkYObBQ3b5gdjWAUXWGootLltgL4mPVv2KZUMaaWa8rFQ4j-Re_PKl0GRX5MeyCn7GVgN6RxPAx0HnBcdMq5glD66h3b6egLE4yZ5-6SBa3pOBG0MHZr_tqeGhMRobVkJ3eAnleHuqr9lcMFDrRHcm1BingJFNsA9N6MMI9lrwZCWZMT-jrkg59JZcRO5MPhfTLysKvlJ_YP2WeX0fdqeTVFWCQveN8zhZrf8h6o-gloBWzdDGeruHlDZ7ipGQGSCNPA4z2rpqwyNIOqersbsfBbL6wbieDs3tWwgYMiH-U4NSp7W1cO1XUg7UC5SK7TtrQC3R9-Q8wxNZ1LxDoF47tszAwws2JlDPRUE7ay90MXE-fxqWM_JKbhfBll9j7zSsrD5EY8jE21qCFji2F0PMouUADZEgdlXcAlRDDtRAGN-_TQpPcc5CEk1uw6o2iV8NvR8PeaBjowJrsPVxo167CFNWO9yz4DVacnx6v_mJ1H3YFs3MznJCq7e0Ajo-bD8OoYlYCelhi2dyBaelUYi7wXlegfallQLIHFc6B4V3x5snvUo1Ov3YM5KPSKsrLdJ9t8gk-_Y6UTPVZp1Zf5o3rzGzpoAxypGVA8V4scOeBAIZHJN-Na_gpsXRl0tNoykf7v-jQP6qaHYIkPOsJt0zEn3Us8twRTc3S__surX8udu6M69objgsDYo1BfUervaapII2AmiqDjoJbUKMmOJ6UuAc2ltMuHsEt2mJTNJ1sUrd-r_7abnvRpI48zQYuBklod6fHdLtv2uqBNUcdN7AkTlKLXd1hphZBqUCc4UlRmaRn9KQD6u0oJKoV3Pn5JvldfwnYjs2m6FrbAh6MrdLTsA_dNE9_7DDN2SGrhFw09HpDY3Dheu4MZkb82hM9A5YhygAbo69DvRh6vjaXzT0qL6_DPjrsrRFKx1ZW5qSFp0DEDgb5D_sFYiC1gDwF2wWcVthoG-lOs3TdU4QntMYgvdbKiP0UTLqNJQpNEy2DjwBqJghZWmJKanHOjo9rlXCctiefBLxhXemPwPZxVBexKYw8VQj-S3Au3cPYqnRupJfKnJyfqATtcnFyyrZkLvLCb4UeeCeRIc5-ze5lWUiKlyuxs-lkyKSV-c7nrw2ftIb4f6PDl9XCb_NouDlz3en6rIXI8pI_KzK8T5yd0RbQ2ciRIZKsoy-VSJXfDuWIMBcExG0UNJUEbIGkg549XcB4bF_R5l_DsEepJED92qdc3qccLBh5lACI1HcN9X1EOZ6k_2HfP5CEzfVw-wBFc_mR7Bb41deRPH8g4gCrBgijOw=}
PROLE_OPENTOFU_URL = http://127.0.0.1:8080
REGISTRY_NAMESPACE = default
SERVICE_NAMESPACE = default
[Welcome]
; No configuration values captured yet for this section.
[Dependencies]
STATUS = All installed
[Network]
AD_DC_HOST = myrddin.prole.org
AD_DC_IP = 10.0.0.3
ANSIBLE_DOMAIN = prole.org
ANSIBLE_INFRASTRUCTURE = ${PROLE_HOME}/infrastructure
ANSIBLE_INVENTORY = ${PROLE_HOME}/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","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://pi.prole.org:6443","server_host":"pi.prole.org","token_present":true},"groups":{"iscsi":["pi.prole.org","raspberry.prole.org","myrddin.prole.org","retropie.prole.org"],"pihole":["pi.prole.org","raspberry.prole.org"],"ad_dc":["myrddin.prole.org"],"k3s_hosts":["pi.prole.org","myrddin.prole.org","retropie.prole.org"],"linux_hosts":["pi.prole.org","raspberry.prole.org","myrddin.prole.org","retropie.prole.org"]},"hosts":{"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":[]}
KDC_ANSIBLE_DETECTED = 10.0.0.3
[Port Forwards]
PORT_FORWARD_EXTERNAL_DB_HOST = myrddin.prole.org
PORT_FORWARD_EXTERNAL_DB_IP = 10.0.0.3
PORT_FORWARD_EXTERNAL_DB_PORT = 5432
PORT_FORWARD_EXTERNAL_DB_SERVICE = prole-db-rw
PORT_FORWARD_K3D_ENABLED = true
PORT_FORWARD_K3D_MAPPING_1 = 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_K3D_MAPPING_10 = id=supabase-kong;namespace=supabase;target=svc/kong;address=0.0.0.0;hostPort=8000;servicePort=8000;protocol=TCP;description=Supabase API (Kong)
PORT_FORWARD_K3D_MAPPING_11 = id=supabase-studio;namespace=supabase;target=svc/studio;address=0.0.0.0;hostPort=18080;servicePort=3000;protocol=TCP;description=Supabase Studio
PORT_FORWARD_K3D_MAPPING_12 = id=supabase-auth;namespace=supabase;target=svc/auth;address=0.0.0.0;hostPort=9999;servicePort=9999;protocol=TCP;description=Supabase Auth (GoTrue)
PORT_FORWARD_K3D_MAPPING_13 = id=supabase-rest;namespace=supabase;target=svc/rest;address=0.0.0.0;hostPort=3001;servicePort=3000;protocol=TCP;description=Supabase REST (PostgREST)
PORT_FORWARD_K3D_MAPPING_14 = id=supabase-realtime;namespace=supabase;target=svc/realtime;address=0.0.0.0;hostPort=4000;servicePort=4000;protocol=TCP;description=Supabase Realtime
PORT_FORWARD_K3D_MAPPING_2 = 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_K3D_MAPPING_3 = id=grafana;namespace=monitoring;target=svc/kps-grafana;address=0.0.0.0;hostPort=3000;servicePort=80;protocol=TCP;description=Grafana UI
PORT_FORWARD_K3D_MAPPING_4 = id=postgres;namespace=${NAMESPACE};target=svc/prole-db-rw;address=0.0.0.0;hostPort=5432;servicePort=5432;protocol=TCP;description=PostgreSQL (primary)
PORT_FORWARD_K3D_MAPPING_5 = id=openbao;namespace=default;target=svc/openbao;address=127.0.0.1;hostPort=8200;servicePort=8200;protocol=TCP;description=OpenBao
PORT_FORWARD_K3D_MAPPING_6 = id=argocd;namespace=argocd;target=svc/argocd-server;address=0.0.0.0;hostPort=8081;servicePort=80;protocol=TCP;description=ArgoCD
PORT_FORWARD_K3D_MAPPING_7 = id=garage;namespace=default;target=svc/garage;address=0.0.0.0;hostPort=3900;servicePort=3900;protocol=TCP;description=Garage S3
PORT_FORWARD_K3D_MAPPING_8 = id=opentofu;namespace=default;target=svc/opentofu;address=0.0.0.0;hostPort=8080;servicePort=8080;protocol=TCP;description=OpenTofu
PORT_FORWARD_K3D_MAPPING_9 = id=prole-db-manager;namespace=${NAMESPACE};target=svc/prole-db-manager;address=0.0.0.0;hostPort=80;servicePort=80;protocol=TCP;description=DB Manager UI
PORT_FORWARD_K3S_ENABLED = true
PORT_FORWARD_K3S_MAPPING_1 = id=grafana;namespace=monitoring;target=svc/kps-grafana;address=0.0.0.0;hostPort=3000;servicePort=3000;protocol=TCP;description=Grafana UI (0.0.0.0:3000)
PORT_FORWARD_K3S_MAPPING_2 = id=opentofu;namespace=${NAMESPACE};target=svc/opentofu;address=0.0.0.0;hostPort=8080;servicePort=8080;protocol=TCP;description=OpenTofu UI/API (0.0.0.0:8080)
PORT_FORWARD_K3S_MAPPING_3 = id=openbao;namespace=${NAMESPACE};target=svc/openbao;address=0.0.0.0;hostPort=8200;servicePort=8200;protocol=TCP;description=OpenVault/OpenBao UI/API (0.0.0.0:8200)
PORT_FORWARD_K3S_MAPPING_4 = id=postgres;namespace=${NAMESPACE};target=svc/prole-db-rw;address=0.0.0.0;hostPort=5432;servicePort=5432;protocol=TCP;description=PostgreSQL external (0.0.0.0:5432)
[System Environment]
PROLE_CONF = ${PROLE_CONF}
PROLE_DATA = ${PROLE_DATA}
PROLE_HOME = ${PROLE_HOME}
PROLE_LOGS = ${PROLE_LOGS}
PROLE_OPENTOFU_URL = http://127.0.0.1:8080
PROLE_SERVICE = ${PROLE_SERVICE}
[Monitoring]
GRAFANA_ADMIN_PASSWORD = ${OPENBAO:kv/prole/${NAMESPACE}/monitoring#grafana_admin_password}
[Kerberos Authentication]
PASSWORD = ${OPENBAO:kv/prole/${NAMESPACE}/kerberos#password}
[Ollama]
; No configuration values captured yet for this section.
[Optional Features]
AT_REST_ENCRYPTION_ENABLED = true
GITOPS_ENABLED = false
KERBEROS_ENABLED = false
SUPABASE_ENABLED = true
[GitOps]
[Database Creation]
DB_NAME = prole-chrisfu-abracadabra
DB_PASSWORD_SET = true
DB_USER = chrisfu
NAMESPACE = prole-chrisfu-abracadabra
[Initialize Cluster]
ENVIRONMENT = k3d-prole-dev-cluster
K3S_SERVER_URL = https://pi.prole.org:6443
K3S_TOKEN = ${PROLE_SECRET:v1:-sPPStEN7GQEkuhI:ozUsHxoQiHCTmYxfZNJHp3yBf91OW-bGtBT1WtuYYvDEUtC4psiUiVU_KJVFlZHdS7iWCEsIitqf_JdygH3aCMloGbh01yVzyswEGVeNiaXGxE8tZ9H5pZ3fENwioPe-gAwXE_sp7yC8sWlfBMrIrRiLo8VAMxYaIQ1Icizvow9fqz38tBCZdQwsefNBoy_mhcbgLEVhCfYdch8EeC2LgmplFj9lPmyW9M22O-5l5EW3IEv901yma__at5nNDbj9Tie9ZPhoGRNnXp68lcNCsfVqS9kevvts-DS-eY5Yo1MH1pDmY_IvC_GME0bNw467lCQ4QFNK8U7R_XSMyYpMreUdCs7p4tvz2vWBJB92bgc7F-FzoAYviTNzsvFyoOiWQ44W4IzJ1ze-dTuSzP70MspglX6yZr18Nc0n1iqS3oTC9240NYgDMGGU2AoMJvb1ejJFmAFAvZketI0OD3bQvkQmk4a9Ej9Z01Fwp1TUH1DHAqmwkV70LrnKrmO7ptmdWTsWll8uE4M755KB3quSQhMzzFhFufXRAFWXTETgP9r-qkVRNCR4gSP-_8AMWUciDBUGjRP3S2xmTXKZwbdvTsaDWylCppqlRFKrqx4hDqtjoKxaYC1LzCEl945BEMTI604gpVfsPy62Vcjikb95EeFoTEM3_nt6c_ROR52U1Hv-V3lOFerSbsj0O5z9PNjwm2216-cPbDiS0-CRBW2XauK0aQjicjOOPyR8__XIbykWuXVfmcVY8pPFDUVjtX3GzsK3ym4l1pUbJ1iQ_L2oFEF6rhOXciaKFhQsy8WIn2SxWQwbYnIkONlc-vHAq1cMIH2AkeMZIteUelKnLSqiC_czmz7bDM4G9L6cO3O0oTVeQ3PLsf6vTsZQUGs6GDKwzPu4DwRpWq0rZqcCTcoC0vDuCMTmGhpBbqLxeApY1jj6QRT-w1-FjEh9ttk_VTwqqm7Lgz8BfdEeBGgsPLRYjqV_cKgQJ5IX0qNJ2To8ekrGdoKkN3h_JTAMBRwU-5PUJmx-ct972hwv8ScCf0-Ap_I7eUKjdQ5UBXv0laK0grFF4SMqmOs5OG5F3kY3Z-9AEi6vDYZzB-NKBTGS8fB0aSz4TrSVp171S5DvkU76HQ23d0tce_358hbD-GzX8i5U9q8YXvgsyVbvFpJMdth1MYmvOa42qGQm8-iYfGHXC0m5CrAPrxXakwupoMYUr1hmhXDSkFWNon02cWWZgK8Fh8gKvdscWCZCZtzijoB27SlxICWav9Ofow4Hb0sYeBg-f8YEJBAjnrKgX1B46OHTZ2Nm76pgDzR24xI=}
[Dev Cluster (k3d)]
CLUSTER_ENV = k3d-prole-dev-cluster
DISPLAY_NAME = prole-dev-cluster
KUBECTL_CONTEXT = k3d-prole-dev-cluster
MODE = k3d
[Service Cluster (k3s)]
CLUSTER_ENV = prole-service-cluster
DISPLAY_NAME = prole-service-cluster
K3S_SERVER_URL = https://pi.prole.org:6443
K3S_TOKEN = ${OPENBAO:kv/prole/${NAMESPACE}/k3s#token}
MODE = k3s
PIPELINE_URL = http://127.0.0.1:8080
[Prod Cluster (k8s)]
ARTIFACTS_DIR = ${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
STATUS = Built
[Initialization Scripts]
STATUS = Attempted
[Deployment]
MODE = k3d
STATUS = Deployed
TARGET = prole-dev-cluster
[Install]
STATUS = Finished

View File

@ -1,202 +0,0 @@
; Prole Master Configuration File
; Generated by install.py on 2026-02-26 01:31:16
; This file is used as input for Ansible deployment and k8s cluster creation.
[User]
; User-editable values; derived values below reference these by default.
NAMESPACE = ${NAMESPACE}
PROLE_CONF = ${PROLE_HOME}/conf
PROLE_DATA = /Users/chrisfu/.prole/data
PROLE_HOME = /Users/chrisfu/dev/prole
PROLE_LOGS = /opt/prole/logs/chrisfu
PROLE_SERVICE = ${PROLE_HOME}/etc
SERVICE_NAMESPACE = default
[Inputs]
; Screen-scoped inputs used for unattended replays (-S)
build.deploy_env = Dev
build.run_build = false
dependencies.ansible.install = true
dependencies.auto_install_missing = true
dependencies.brew.install = true
dependencies.docker.install = true
dependencies.k3d.install = true
dependencies.kubectl.install = true
dependencies.opentofu.install = true
dependencies.python.install = true
dependencies.verify_all = false
disk_selection.disk_type = local
disk_selection.local_path = ${PROLE_HOME}/prole-tools-app/dist
disk_selection.removable_mount =
env_setup.NAMESPACE = ${NAMESPACE}
env_setup.PROLE_CONF = ${PROLE_CONF}
env_setup.PROLE_DATA = ${PROLE_DATA}
env_setup.PROLE_HOME = ${PROLE_HOME}
env_setup.PROLE_LOGS = ${PROLE_LOGS}
env_setup.PROLE_SERVICE = ${PROLE_SERVICE}
init_cluster.at_rest_encryption_enabled = true
init_cluster.cluster_env = k3d-prole-dev-cluster
init_cluster.gitops_enabled = false
init_cluster.k3s_server_url = https://pi.prole.org:6443
init_cluster.k3s_token = ${OPENBAO:kv/prole/${NAMESPACE}/k3s#token}
init_cluster.kerberos_enabled = false
init_cluster.start_cluster = true
init_cluster.supabase_enabled = true
init_cnpg_deploy.force_rollout = false
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 = ${OPENBAO:kv/prole/${NAMESPACE}/db#password}
init_password.db_password_confirm = ${OPENBAO:kv/prole/${NAMESPACE}/db#password}
init_password.db_username = chrisfu
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.205
kerberos_config.password = ${OPENBAO:kv/prole/${NAMESPACE}/kerberos#password}
kerberos_config.realm = PROLE.ORG
kerberos_config.test_connection = false
kerberos_config.user =
network_scan.run = true
ollama_config.model =
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 = k3d-prole-dev-cluster
DB_HOST_PORT = 5432
DB_PASSWORD = ${OPENBAO:kv/prole/${NAMESPACE}/db#password}
DEPLOYMENT_MODE = k3d
DEPLOYMENT_TARGET = prole-dev-cluster
DOCKER_IMPORT_DIR =
NAMESPACE = ${NAMESPACE}
PROLE_DB_USER = chrisfu
PROLE_ENABLE_LOCAL_REGISTRY = true
PROLE_HOME = ${PROLE_HOME}
PROLE_K3S_SERVER = https://pi.prole.org:6443
PROLE_K3S_TOKEN = ${PROLE_SECRET:v1:7PxbzYh-J2MWFvEr:4HQYe1HI1OOkYObBQ3b5gdjWAUXWGootLltgL4mPVv2KZUMaaWa8rFQ4j-Re_PKl0GRX5MeyCn7GVgN6RxPAx0HnBcdMq5glD66h3b6egLE4yZ5-6SBa3pOBG0MHZr_tqeGhMRobVkJ3eAnleHuqr9lcMFDrRHcm1BingJFNsA9N6MMI9lrwZCWZMT-jrkg59JZcRO5MPhfTLysKvlJ_YP2WeX0fdqeTVFWCQveN8zhZrf8h6o-gloBWzdDGeruHlDZ7ipGQGSCNPA4z2rpqwyNIOqersbsfBbL6wbieDs3tWwgYMiH-U4NSp7W1cO1XUg7UC5SK7TtrQC3R9-Q8wxNZ1LxDoF47tszAwws2JlDPRUE7ay90MXE-fxqWM_JKbhfBll9j7zSsrD5EY8jE21qCFji2F0PMouUADZEgdlXcAlRDDtRAGN-_TQpPcc5CEk1uw6o2iV8NvR8PeaBjowJrsPVxo167CFNWO9yz4DVacnx6v_mJ1H3YFs3MznJCq7e0Ajo-bD8OoYlYCelhi2dyBaelUYi7wXlegfallQLIHFc6B4V3x5snvUo1Ov3YM5KPSKsrLdJ9t8gk-_Y6UTPVZp1Zf5o3rzGzpoAxypGVA8V4scOeBAIZHJN-Na_gpsXRl0tNoykf7v-jQP6qaHYIkPOsJt0zEn3Us8twRTc3S__surX8udu6M69objgsDYo1BfUervaapII2AmiqDjoJbUKMmOJ6UuAc2ltMuHsEt2mJTNJ1sUrd-r_7abnvRpI48zQYuBklod6fHdLtv2uqBNUcdN7AkTlKLXd1hphZBqUCc4UlRmaRn9KQD6u0oJKoV3Pn5JvldfwnYjs2m6FrbAh6MrdLTsA_dNE9_7DDN2SGrhFw09HpDY3Dheu4MZkb82hM9A5YhygAbo69DvRh6vjaXzT0qL6_DPjrsrRFKx1ZW5qSFp0DEDgb5D_sFYiC1gDwF2wWcVthoG-lOs3TdU4QntMYgvdbKiP0UTLqNJQpNEy2DjwBqJghZWmJKanHOjo9rlXCctiefBLxhXemPwPZxVBexKYw8VQj-S3Au3cPYqnRupJfKnJyfqATtcnFyyrZkLvLCb4UeeCeRIc5-ze5lWUiKlyuxs-lkyKSV-c7nrw2ftIb4f6PDl9XCb_NouDlz3en6rIXI8pI_KzK8T5yd0RbQ2ciRIZKsoy-VSJXfDuWIMBcExG0UNJUEbIGkg549XcB4bF_R5l_DsEepJED92qdc3qccLBh5lACI1HcN9X1EOZ6k_2HfP5CEzfVw-wBFc_mR7Bb41deRPH8g4gCrBgijOw=}
PROLE_OPENTOFU_URL = http://127.0.0.1:8080
REGISTRY_NAMESPACE = default
SERVICE_NAMESPACE = default
[Welcome]
; No configuration values captured yet for this section.
[Dependencies]
STATUS = All installed
[Network]
AD_DC_HOST = myrddin.prole.org
AD_DC_IP = 10.0.0.3
ANSIBLE_DOMAIN = prole.org
ANSIBLE_INFRASTRUCTURE = ${PROLE_HOME}/infrastructure
ANSIBLE_INVENTORY = ${PROLE_HOME}/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","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://pi.prole.org:6443","server_host":"pi.prole.org","token_present":true},"groups":{"iscsi":["pi.prole.org","raspberry.prole.org","myrddin.prole.org","retropie.prole.org"],"pihole":["pi.prole.org","raspberry.prole.org"],"ad_dc":["myrddin.prole.org"],"k3s_hosts":["pi.prole.org","myrddin.prole.org","retropie.prole.org"],"linux_hosts":["pi.prole.org","raspberry.prole.org","myrddin.prole.org","retropie.prole.org"]},"hosts":{"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":[]}
KDC_ANSIBLE_DETECTED = 10.0.0.3
KDC_AUTO_DETECTED = 10.0.0.205
KERBEROS_AUTO_ENABLED = True
[Port Forwards]
PORT_FORWARD_EXTERNAL_DB_HOST = myrddin.prole.org
PORT_FORWARD_EXTERNAL_DB_IP = 10.0.0.3
PORT_FORWARD_EXTERNAL_DB_PORT = 5432
PORT_FORWARD_EXTERNAL_DB_SERVICE = prole-db-rw
PORT_FORWARD_K3D_ENABLED = true
PORT_FORWARD_K3D_MAPPING_1 = 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_K3D_MAPPING_10 = id=supabase-kong;namespace=supabase;target=svc/kong;address=0.0.0.0;hostPort=8000;servicePort=8000;protocol=TCP;description=Supabase API (Kong)
PORT_FORWARD_K3D_MAPPING_11 = id=supabase-studio;namespace=supabase;target=svc/studio;address=0.0.0.0;hostPort=18080;servicePort=3000;protocol=TCP;description=Supabase Studio
PORT_FORWARD_K3D_MAPPING_12 = id=supabase-auth;namespace=supabase;target=svc/auth;address=0.0.0.0;hostPort=9999;servicePort=9999;protocol=TCP;description=Supabase Auth (GoTrue)
PORT_FORWARD_K3D_MAPPING_13 = id=supabase-rest;namespace=supabase;target=svc/rest;address=0.0.0.0;hostPort=3001;servicePort=3000;protocol=TCP;description=Supabase REST (PostgREST)
PORT_FORWARD_K3D_MAPPING_14 = id=supabase-realtime;namespace=supabase;target=svc/realtime;address=0.0.0.0;hostPort=4000;servicePort=4000;protocol=TCP;description=Supabase Realtime
PORT_FORWARD_K3D_MAPPING_2 = 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_K3D_MAPPING_3 = id=grafana;namespace=monitoring;target=svc/kps-grafana;address=0.0.0.0;hostPort=3000;servicePort=80;protocol=TCP;description=Grafana UI
PORT_FORWARD_K3D_MAPPING_4 = id=postgres;namespace=${NAMESPACE};target=svc/prole-db-rw;address=0.0.0.0;hostPort=5432;servicePort=5432;protocol=TCP;description=PostgreSQL (primary)
PORT_FORWARD_K3D_MAPPING_5 = id=openbao;namespace=default;target=svc/openbao;address=127.0.0.1;hostPort=8200;servicePort=8200;protocol=TCP;description=OpenBao
PORT_FORWARD_K3D_MAPPING_6 = id=argocd;namespace=argocd;target=svc/argocd-server;address=0.0.0.0;hostPort=8081;servicePort=80;protocol=TCP;description=ArgoCD
PORT_FORWARD_K3D_MAPPING_7 = id=garage;namespace=default;target=svc/garage;address=0.0.0.0;hostPort=3900;servicePort=3900;protocol=TCP;description=Garage S3
PORT_FORWARD_K3D_MAPPING_8 = id=opentofu;namespace=default;target=svc/opentofu;address=0.0.0.0;hostPort=8080;servicePort=8080;protocol=TCP;description=OpenTofu
PORT_FORWARD_K3D_MAPPING_9 = id=prole-db-manager;namespace=${NAMESPACE};target=svc/prole-db-manager;address=0.0.0.0;hostPort=80;servicePort=80;protocol=TCP;description=DB Manager UI
PORT_FORWARD_K3S_ENABLED = true
PORT_FORWARD_K3S_MAPPING_1 = id=grafana;namespace=monitoring;target=svc/kps-grafana;address=0.0.0.0;hostPort=3000;servicePort=3000;protocol=TCP;description=Grafana UI (0.0.0.0:3000)
PORT_FORWARD_K3S_MAPPING_2 = id=opentofu;namespace=${NAMESPACE};target=svc/opentofu;address=0.0.0.0;hostPort=8080;servicePort=8080;protocol=TCP;description=OpenTofu UI/API (0.0.0.0:8080)
PORT_FORWARD_K3S_MAPPING_3 = id=openbao;namespace=${NAMESPACE};target=svc/openbao;address=0.0.0.0;hostPort=8200;servicePort=8200;protocol=TCP;description=OpenVault/OpenBao UI/API (0.0.0.0:8200)
PORT_FORWARD_K3S_MAPPING_4 = id=postgres;namespace=${NAMESPACE};target=svc/prole-db-rw;address=0.0.0.0;hostPort=5432;servicePort=5432;protocol=TCP;description=PostgreSQL external (0.0.0.0:5432)
[System Environment]
NAMESPACE = ${NAMESPACE}
PROLE_CONF = ${PROLE_CONF}
PROLE_DATA = ${PROLE_DATA}
PROLE_HOME = ${PROLE_HOME}
PROLE_LOGS = ${PROLE_LOGS}
PROLE_OPENTOFU_URL = http://127.0.0.1:8080
PROLE_SERVICE = ${PROLE_SERVICE}
[Monitoring]
GRAFANA_ADMIN_PASSWORD = ${OPENBAO:kv/prole/${NAMESPACE}/monitoring#grafana_admin_password}
[Kerberos Authentication]
PASSWORD = ${OPENBAO:kv/prole/${NAMESPACE}/kerberos#password}
[Ollama]
; No configuration values captured yet for this section.
[Optional Features]
AT_REST_ENCRYPTION_ENABLED = true
GITOPS_ENABLED = false
KERBEROS_ENABLED = false
SUPABASE_ENABLED = true
[GitOps]
[Database Creation]
DB_NAME = ${NAMESPACE}
DB_PASSWORD_SET = true
DB_USER = chrisfu
NAMESPACE = ${NAMESPACE}
[Initialize Cluster]
ENVIRONMENT = k3d-prole-dev-cluster
K3S_SERVER_URL = https://pi.prole.org:6443
K3S_TOKEN = ${PROLE_SECRET:v1:-sPPStEN7GQEkuhI:ozUsHxoQiHCTmYxfZNJHp3yBf91OW-bGtBT1WtuYYvDEUtC4psiUiVU_KJVFlZHdS7iWCEsIitqf_JdygH3aCMloGbh01yVzyswEGVeNiaXGxE8tZ9H5pZ3fENwioPe-gAwXE_sp7yC8sWlfBMrIrRiLo8VAMxYaIQ1Icizvow9fqz38tBCZdQwsefNBoy_mhcbgLEVhCfYdch8EeC2LgmplFj9lPmyW9M22O-5l5EW3IEv901yma__at5nNDbj9Tie9ZPhoGRNnXp68lcNCsfVqS9kevvts-DS-eY5Yo1MH1pDmY_IvC_GME0bNw467lCQ4QFNK8U7R_XSMyYpMreUdCs7p4tvz2vWBJB92bgc7F-FzoAYviTNzsvFyoOiWQ44W4IzJ1ze-dTuSzP70MspglX6yZr18Nc0n1iqS3oTC9240NYgDMGGU2AoMJvb1ejJFmAFAvZketI0OD3bQvkQmk4a9Ej9Z01Fwp1TUH1DHAqmwkV70LrnKrmO7ptmdWTsWll8uE4M755KB3quSQhMzzFhFufXRAFWXTETgP9r-qkVRNCR4gSP-_8AMWUciDBUGjRP3S2xmTXKZwbdvTsaDWylCppqlRFKrqx4hDqtjoKxaYC1LzCEl945BEMTI604gpVfsPy62Vcjikb95EeFoTEM3_nt6c_ROR52U1Hv-V3lOFerSbsj0O5z9PNjwm2216-cPbDiS0-CRBW2XauK0aQjicjOOPyR8__XIbykWuXVfmcVY8pPFDUVjtX3GzsK3ym4l1pUbJ1iQ_L2oFEF6rhOXciaKFhQsy8WIn2SxWQwbYnIkONlc-vHAq1cMIH2AkeMZIteUelKnLSqiC_czmz7bDM4G9L6cO3O0oTVeQ3PLsf6vTsZQUGs6GDKwzPu4DwRpWq0rZqcCTcoC0vDuCMTmGhpBbqLxeApY1jj6QRT-w1-FjEh9ttk_VTwqqm7Lgz8BfdEeBGgsPLRYjqV_cKgQJ5IX0qNJ2To8ekrGdoKkN3h_JTAMBRwU-5PUJmx-ct972hwv8ScCf0-Ap_I7eUKjdQ5UBXv0laK0grFF4SMqmOs5OG5F3kY3Z-9AEi6vDYZzB-NKBTGS8fB0aSz4TrSVp171S5DvkU76HQ23d0tce_358hbD-GzX8i5U9q8YXvgsyVbvFpJMdth1MYmvOa42qGQm8-iYfGHXC0m5CrAPrxXakwupoMYUr1hmhXDSkFWNon02cWWZgK8Fh8gKvdscWCZCZtzijoB27SlxICWav9Ofow4Hb0sYeBg-f8YEJBAjnrKgX1B46OHTZ2Nm76pgDzR24xI=}
[Dev Cluster (k3d)]
CLUSTER_ENV = k3d-prole-dev-cluster
DISPLAY_NAME = prole-dev-cluster
KUBECTL_CONTEXT = k3d-prole-dev-cluster
MODE = k3d
[Service Cluster (k3s)]
CLUSTER_ENV = prole-service-cluster
DISPLAY_NAME = prole-service-cluster
K3S_SERVER_URL = https://pi.prole.org:6443
K3S_TOKEN = ${OPENBAO:kv/prole/${NAMESPACE}/k3s#token}
MODE = k3s
PIPELINE_URL = http://127.0.0.1:8080
[Prod Cluster (k8s)]
ARTIFACTS_DIR = ${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
STATUS = Built
[Initialization Scripts]
STATUS = Attempted
[Deployment]
MODE = k3d
STATUS = Deployed
TARGET = prole-dev-cluster
[Install]
STATUS = Failed

View File

@ -1,176 +0,0 @@
; Prole Master Configuration File
; Generated by install.py on 2026-02-06 00:16:41
; This file is used as input for Ansible deployment and k8s cluster creation.
[User]
; User-editable values; derived values below reference these by default.
PROLE_HOME = /Users/chrisfu/dev/prole
PROLE_CONF = ${PROLE_HOME}/conf
PROLE_SERVICE = ${PROLE_HOME}/etc
PROLE_DATA = /opt/prole/data/chrisfu
PROLE_LOGS = /opt/prole/logs/chrisfu
NAMESPACE = ${NAMESPACE}
SERVICE_NAMESPACE = default
[Inputs]
; Screen-scoped inputs used for unattended replays (-S)
build.deploy_env = Dev
build.run_build = false
dependencies.auto_install_missing = true
dependencies.brew.install = true
dependencies.docker.install = true
dependencies.k3d.install = true
dependencies.verify_all = false
disk_selection.disk_type = local
disk_selection.local_path = ${PROLE_HOME}/prole-tools-app/dist
disk_selection.removable_mount =
env_setup.NAMESPACE = ${NAMESPACE}
env_setup.PROLE_CONF = ${PROLE_CONF}
env_setup.PROLE_DATA = ${PROLE_DATA}
env_setup.PROLE_HOME = ${PROLE_HOME}
env_setup.PROLE_LOGS = ${PROLE_LOGS}
env_setup.PROLE_SERVICE = ${PROLE_SERVICE}
init_cluster.at_rest_encryption_enabled = true
init_cluster.cluster_env = prole-service-cluster
init_cluster.k3s_server_url = https://pi.prole.org:6443
init_cluster.k3s_token = ${PROLE_SECRET:v1:D0LdPKBDiJ7v_-Ki:bcz3775FNoxa-EH8iuCF2xrGu7iBHf7ZLPCNojWKYNVDKi85PpRa6NeTWwFLfJjq0mkpNvUSPgv-7xqjCjt4rZ-PW-jb3HLGkxQ1jTo-O_nLSAWPX9ltbkU-rZqohCmzQspJhqGgGpSnYo_1QP78QqOhMHzpRCg2Jnoyocd7S_mJdIR4scTWa0mZUmWKqjHcGKAalfdWqTSVtcexiWYJMuw7CnsNDRLFT-B_qQAmtrb-9skUcmAeLf19WwNxiE9Hw5RAfkF-7lbcA4jr4DyYesg5ZNSN3cGbMscXhs1ml4WpK1gOomiGO4JKegRv5Br4yKrdbfOIIG3bSdlBytbtX4oLmQB76dtKzka7QqBre2e30EEKV8vLsDrm62_0tYTeSfXWqzsrfeMBwz4cL20ihxAM7u1RjbigtRIndggARGq0SJUnffw4diz6T6xHEx5EFuMgObpOrOwbm5YMZqrvOsEEgtT-IAmrVjGkLpZqkyaLUfrbqMkaG8jgKn6TR7bNfW0fHnFzmBHc3lLKOgUtIHGMbEYTpWijVARQx4YcsSejSPsnIf-p5dhpQrRZbw4XFxMQRzN70eelYDx5LhekkBlSUiRutcs8_lTKYrmP7QKtGnxXSxqh9cZYD4hacFhq8gvOk8QppBRa-tHt3dE-CRnULRd7g0exoCKaex8kbldMs3HUhTTFGFlNKBS2xoSB1DBG0lcfDEAIF0AbsZ6Zz7iW3K72DvpyxsYhVevMzC57ExRBVnuezSzkkuvAdQpQs_07yMDRURDdxOxbH67lySjrDbwLH70s1AvA_MflcTj4WmrbEOycvTVRh5YUdgKkDnuteN4Qy6xdUpHr2xquNq_JKy3d90ZaOZeSiR4XhB8O2fxtwM5ArrAUpATJtxsYcZeFM7RW_6arfRhbt_XMeOXk-T1J5dtGMAHfOp5MOQuwrW-k7nAunr8X85N2SkmEi9feYNgGV-UFlb-ME0sAi5ejS-W8bmYgCU6Xfc0qUwVYFnXcjoEXV9bjnQkcDRtJXh2WQ1XjxJPOMWhmmoW9mCnvfwxH29io19DxPfECTj-gN8bshyK1Va59iN70YG39Uz4-cEshZG83OJTXdbkDiaQk7HpJJT31hiW8scgTV2rSFhT19AmO_aQ0XSfXzzQ2QQu79q1fkTpz5_6Jrgzs3oVTaT6jvLMSVJzYoEmZuqkuVagOJBCO8woLE8qN1-2SfMWCYkTU7p9yfVFilT12Se8M0ebk2-M0Lnr8NWexmqI6SJxRVygz6c8pck8TBKZL2D3PrOliV_Zk3kVhZ48oN6fGScJdC8djJCI=}
init_cluster.kerberos_enabled = false
init_cluster.start_cluster = true
init_cluster.supabase_enabled = false
init_cnpg_deploy.force_rollout = false
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 = ${OPENBAO:kv/prole/prole-chrisfu-abracadabra/db#password}
init_password.db_password_confirm = ${OPENBAO:kv/prole/prole-chrisfu-abracadabra/db#password}
init_password.db_username = chrisfu
init_password.generate_ssh_key = true
init_scripts.run_scripts = true
kerberos_config.enabled = false
kerberos_config.kdc = 10.0.0.3
kerberos_config.password = ${OPENBAO:kv/prole/prole-chrisfu-abracadabra/kerberos#password}
kerberos_config.realm = PROLE.ORG
kerberos_config.test_connection = false
kerberos_config.user =
network_scan.run = true
[Global]
; Variables used by name in more than one place or assumed global scope
CLUSTER_ENV = prole-service-cluster
DB_HOST_PORT = 5432
DB_PASSWORD = ${OPENBAO:kv/prole/prole-chrisfu-abracadabra/db#password}
DEPLOYMENT_MODE = k3s
DEPLOYMENT_TARGET = prole-service-cluster
DOCKER_IMPORT_DIR =
NAMESPACE = ${NAMESPACE}
PROLE_DB_USER = chrisfu
PROLE_HOME = ${PROLE_HOME}
PROLE_K3S_SERVER = https://pi.prole.org:6443
PROLE_K3S_TOKEN = ${PROLE_SECRET:v1:ZD9jqhOX6ouE2aLU:u4zRLPSMHFIfJYoLq431CXWYEorGuWSD4QU8VjArBdVFRXUuw57wayOzVDY82d_kREyGqtl95qIR3W-LXh05xXCh2NIpw15vLwlHBToW15tguDU1PzXRFLA669ZPYdPDFw_02fyKnPOYpELVmxWwP1U3DAGhKiTMqBDNt4oWc5pVzhTf7SO96uzUXoNG4_tQQ8H3NuSpKcg4eFbuh3lwnKmqY7m-9ofT-veIHGdXUEh0bTTamI9Qa-x1VjhwP6ZUv-VrobxTIUGd-8qUmfSPBr-U-jaOk_gvUvuyG-aTROcFrh36flmP__fLHCUwW_kAy6PZJNEPLIjgaPVBp4esjcVbPqJyiArlH8vr_hkDKhRoFJ9uagbABxuyzemTgNylVrSBHxEqIlFiQ6HncILm9s9FpLvotfDZUcG03V45vE3aV9BQsFJMfPtrkz_dVm5AuubGdG8NclV8XyRSfRy3efwPSz3wNuEkYDFJ7ZM-CGFMBz-DTlcAWVTxNXnXMyD7uK4a-13XKj_9IKijKIJ4EKuuxUr5dozvmhB90jmzFV4DYM4aiZIT6zrhARnrWkGtgRZDx89tssT20aXvPNjKy1wFcAgvqdZuqCQYlDTnhxPoJt-BEAC1xcJVJp5mnv8YW7hjJy1hA7u8S0AvCdB4eoh7vnWqLWVG1GMkgpU06641EEl3DksNZH1bbud6jV08T2SrmkRFnnj8RyoDRJ4aH8Z7uyyINV57A8bgvDsDECfQ6Iu-4WXo4ERhKdA5AJPMNW8LZexnHq1DDU4F9zDyNoaMOpBBLjyWKMsUo3fAIe1BF5PUQi4Ea4QqJq560GaoUxk5DhT38kSaDYmX3z1BB-QxzN66FQJ6QAPTLoTCllilc5j5RaSGbHiFPxyYB4H1w1boQv8sNfTi7GETkcjcCoJ4wpHwJLxDcaKvZvNxoJBACS8s-3YHEWnFe4APrDTRNCiKu1UbadbWRDK-dkNoM0KLta4SWG8eXXkDVwPV23q_Y6OJw_71g2JH2i2iT22lP3WcrsZfmpeVwGJYVsZlvbGAC_mH33qljdYtXyIfTBihh6x_flm-ogJEPuFUGIn92cF76hIqIjyz6VBf-W4y0sheU_pBd2eOZSKEFV1j4Xz8CHC1HtrtV0IFDGaXUS1kfccYoU40mczaXQxTFYtqDf-_azZyoh6I2_oQLtRX_A49NgTBpbrT0NLxFVEqXcce3QtiMsiMhV7G4K_HLPRalolqfr9x3WghS6ac8F_7KbINkXywDAgZGE7dgzr8V8Cf4ezXSNkJ7sZmDSkEBp8DO-WjZg5NIr1TMy0=}
PROLE_OPENTOFU_URL = http://127.0.0.1:8080
SERVICE_NAMESPACE = default
ARGOCD_NAMESPACE = argocd
REGISTRY_NAMESPACE = default
PROLE_ENABLE_LOCAL_REGISTRY = true
[Welcome]
; No configuration values captured yet for this section.
[Dependencies]
STATUS = All installed
[Network]
AD_DC_HOST = myrddin.prole.org
AD_DC_IP = 10.0.0.3
ANSIBLE_DOMAIN = prole.org
ANSIBLE_INFRASTRUCTURE = ${PROLE_HOME}/infrastructure
ANSIBLE_INVENTORY = ${PROLE_HOME}/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","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://pi.prole.org:6443","server_host":"pi.prole.org","token_present":false},"groups":{"iscsi":["pi.prole.org","raspberry.prole.org","myrddin.prole.org","retropie.prole.org"],"pihole":["pi.prole.org","raspberry.prole.org"],"ad_dc":["myrddin.prole.org"],"k3s_hosts":["pi.prole.org","myrddin.prole.org","retropie.prole.org"],"linux_hosts":["pi.prole.org","raspberry.prole.org","myrddin.prole.org","retropie.prole.org"]},"hosts":{"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":[]}
KDC_ANSIBLE_DETECTED = 10.0.0.3
[Port Forwards]
; Port-forward defaults for local k3d and external k3s access.
PORT_FORWARD_K3D_ENABLED = true
PORT_FORWARD_K3S_ENABLED = true
PORT_FORWARD_EXTERNAL_DB_HOST = myrddin.prole.org
PORT_FORWARD_EXTERNAL_DB_IP = 10.0.0.3
PORT_FORWARD_EXTERNAL_DB_PORT = 5432
PORT_FORWARD_EXTERNAL_DB_SERVICE = prole-db-rw
; Mapping format: id=...;namespace=...;target=...;address=...;hostPort=...;servicePort=...;protocol=TCP;description=...
PORT_FORWARD_K3D_MAPPING_1 = 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 (https://127.0.0.1:8443)
PORT_FORWARD_K3D_MAPPING_2 = 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 (http://127.0.0.1:9090)
PORT_FORWARD_K3D_MAPPING_3 = id=grafana;namespace=monitoring;target=svc/kps-grafana;address=0.0.0.0;hostPort=3000;servicePort=3000;protocol=TCP;description=Grafana UI (0.0.0.0:3000)
PORT_FORWARD_K3D_MAPPING_4 = id=postgres;namespace=${NAMESPACE};target=svc/prole-db-rw;address=0.0.0.0;hostPort=5432;servicePort=5432;protocol=TCP;description=PostgreSQL (primary) (0.0.0.0:5432)
PORT_FORWARD_K3D_MAPPING_5 = id=openbao;namespace=${NAMESPACE};target=svc/openbao;address=127.0.0.1;hostPort=8200;servicePort=8200;protocol=TCP;description=OpenBao UI (http://127.0.0.1:8200)
PORT_FORWARD_K3S_MAPPING_1 = id=grafana;namespace=monitoring;target=svc/kps-grafana;address=0.0.0.0;hostPort=3000;servicePort=3000;protocol=TCP;description=Grafana UI (0.0.0.0:3000)
PORT_FORWARD_K3S_MAPPING_2 = id=opentofu;namespace=${NAMESPACE};target=svc/opentofu;address=0.0.0.0;hostPort=8080;servicePort=8080;protocol=TCP;description=OpenTofu UI/API (0.0.0.0:8080)
PORT_FORWARD_K3S_MAPPING_3 = id=openbao;namespace=${NAMESPACE};target=svc/openbao;address=0.0.0.0;hostPort=8200;servicePort=8200;protocol=TCP;description=OpenVault/OpenBao UI/API (0.0.0.0:8200)
PORT_FORWARD_K3S_MAPPING_4 = id=postgres;namespace=${NAMESPACE};target=svc/prole-db-rw;address=0.0.0.0;hostPort=5432;servicePort=5432;protocol=TCP;description=PostgreSQL external (0.0.0.0:5432)
[System Environment]
PROLE_CONF = ${PROLE_CONF}
PROLE_DATA = ${PROLE_DATA}
PROLE_HOME = ${PROLE_HOME}
PROLE_LOGS = ${PROLE_LOGS}
PROLE_OPENTOFU_URL = http://127.0.0.1:8080
PROLE_SERVICE = ${PROLE_SERVICE}
[Monitoring]
GRAFANA_ADMIN_PASSWORD = ${OPENBAO:kv/prole/prole-chrisfu-abracadabra/monitoring#grafana_admin_password}
[Kerberos Authentication]
PASSWORD = ${OPENBAO:kv/prole/prole-chrisfu-abracadabra/kerberos#password}
[Optional Features]
AT_REST_ENCRYPTION_ENABLED = true
KERBEROS_ENABLED = false
SUPABASE_ENABLED = false
[Database Creation]
DB_NAME = prole-chrisfu-abracadabra
DB_PASSWORD_SET = true
DB_USER = chrisfu
NAMESPACE = prole-chrisfu-abracadabra
[Initialize Cluster]
ENVIRONMENT = prole-service-cluster
K3S_SERVER_URL = https://pi.prole.org:6443
K3S_TOKEN = ${PROLE_SECRET:v1:ZdE0r6mZSNW939fS:9OMnHHaxB8SpdJIf_8hLu5G675IbqVXkB0RfeAlZz8sicJCIsccAdkdIouSCPNeWPwxZb_D6jPj2ARF_2fEYyFGO4k809M6jVNyj44it43of5b_-vMhFYcH36lmnMaeY3WdEWsZ6Xitojy0od5QRm-ovGdPeyLpNvYuUYhLND0HML-Yat8SH-6hGDhoXJWrWJbxD0f4dY7kBDQYeucQZJPmxBgcQARs2LWzmnaEkJQAMJxrqL0VGHVC_DVdFrKgRPJukSQGpw8hI9fw40400q1MIcHYbdJO4Up5reEo-92ZOhkdc2jx35MQrcoq1_3BpEOiwZtBgnX5k98Iccm9NVSqVmH9RqTFnDiIHuolpz2NRoHhaqawp8fDLcewOL4q2UjtqjvQMG7wa1zEkJAURdBGAGfMx607R6OJ48xRHuOz3Pr_C3qouXtn5t9_vr-byJZh6AdKbCotwr93vON3NgS5kStn40ucqO3Ess4d2sLyssekjuktBKLMYjL90UOCgzdv3BxkdCi4ULXgu_6fshf2pCLHwAdjRiJLZkzeWT5bu8z4t60WvBxKqz2KoGny601Vn5E2SdGKtT0IBzW5Wy3oPuDyeQ6sE3xIr3_lcaBr7madtIIwKBTJzLuunFXcY9NaBzwqvgMMGYZoDPEgwpEN7oMJ7Xngf5j9vxWKLXBQPGpeWrqTm0hpGCP2wu3ZIfDM2cd04Au2gRXYpAlRn_ItXHHFIsp8W9LS5pAa7yDe3I1y4IvH67lDD1ncBJi4eEzIAnjSyeHvodnwMGmwYXqgsDmr1dqoYJgF98iDZRQ71tnTZbQWpw6jnhBp2SwTlzmdiG_vE2HbBkLjkF7E0NQiR9hQO6FxjV5qf0UGCSYl_biaYfoIQiNaukneBZAxljZJqMaQzcpw4JoDs82n8SFt5af9ajA_4uD7-ydiBO86v0hhkp6RKvX2Xbi9lnrUmJCkc0d9cUegzwa0F4_9K0e6u0fmGN8nczt9o46L6XlXyUkmbtlMRpGakfSsHOVrpAOuEZW3osXNfnMJ2pdjs_RSuKc9wxQua6yTQbzNWCtmbjYXhApwE_a7YxVwA77Eon-p_uTIiPRnZ3ECxidTR0vJMXJBj-4QtkEo8f3GVSIg_BbHbJjfyZj9KQ7GK7zrUcJTDCOd8h3Nka9ydxivPU3wLG01h-8nSJTlww_9wh3jjyXcu2-bQU2bnmnMAbWPpuQYR99Zfu8mzXK6UpQ-lC7oNFmc6L7WGdhgspWF0yQZLgYtuHQRScscCADpCLE0oxCjMQ0sfrmlxJBHkB_9OVCVna2EOfXL9_x8=}
[Dev Cluster (k3d)]
CLUSTER_ENV = k3d-prole-dev-cluster
DISPLAY_NAME = prole-dev-cluster
KUBECTL_CONTEXT = prole-service-cluster
MODE = k3d
[Service Cluster (k3s)]
CLUSTER_ENV = prole-service-cluster
DISPLAY_NAME = prole-service-cluster
K3S_SERVER_URL = https://pi.prole.org:6443
K3S_TOKEN = ${PROLE_SECRET:v1:kVESNMHu2Zlj7wMY:LD8gNX0Pc81llPAAB73o8Ytb91VZO6RZIwikfpQ55aW9xDtzRatNMY3hwYYyPvdfCmbJm8sQjckowNrxNYeFbiBsbEL6SDVx510aYabi_VgkLTWiyecr4SpCKRhtsZXJ6tKN3PGfRP6e6_jT1M0npeP_wYYXVxfCDdiyGKzZd-Jr2XYDZnyI0A6iyNVc7KZIpE0MNXsI0VKpPckLX8nI2i9WraeNlx5DTcbhbT3Ne_p75vNgVGvWaGFJLStDfnGHhkThWiC5nFp0oqckGzUkUqP1afrzdon2wqiflwTfamN-AFnw1gnwkS9z8LY7R8UhJQrxb5NYCfZVbQn-XJbdHMecujPOdpVP97gHLnwD60qizT4YCs9u6iD-T7ARvxjscpHX7dGocj3ZVeXYFK5ypmEIVT6U20gL674yebMniMz3hHMq4AEDKGfEWGZXiaY3o-y9_dorC7VmGZAn-1_FHvg6h_LP3ybCKybohm8EdmKFKfOYC_RbaNGk43Xz_aGfeq6GNZ11XmBP-5rQwHXAl0vO8aJyDD_Eruci5aSZAg0Y5fevmSszsxI0DJQYgZEB_w1yUtX6jCk5fJLdXnKartU0sYVYqtUVuCvWxmlTXBjo6IaGVnH48Pwy_yDXmjhI1aSGax-7wTFVOH5cfyvMs-vvFsw1WyF1r71a1kRaSpK80zJdDHSoxc5GAfV_P1L1cbgRbB2s08VF4r_CaL3jQ3KDc07rEDJ-Jpxe7VyH7p_beOFzbZfUvCrLswTHyi068gIsxVoMjMxRZJ7tlkM_EqoY2omZukF9564Lq0kZrkyRHdunKHOMhxr0mkFk7pos0gHE5psylgjMqQtcIrPeEYMpqlJt6jGoAmyN2iM9YaZT7ZRKWPEzonBvGx0HwktEWrOqobLn0BHy1gwdikaW80vC4Ao6F8-oGaqpEL4wAMRZTzDnCx6g2BophU5AFfD9DCjT5C5Pk4UunPh2sLFrtnFIsY8_UKt8u9L5KxPV_ty9OSxMqxCBh1Xq-QSHG7yhKkZaLudX9srlJkTJp5BHtIkAqzeO3LKwNTJDWcw29tG9E_ak-qM4mBl65V_XiPKW7Trai8zfwS1KBsgEO8aGVpn9fjDm3UfwD--q5Xl3PMBVTmrpLqTla1RCPLD-IpvAOlssNoTeoGo7cjDmXEwu110Wu1m6W0snsRxVNY5DniHyw6HDPvZqsyLLz5PojQ06jnTvdIYoKorT4CBbCyHtFhuvo_jlrHnc-N1YVSdGD1Bi7nKoXtgeEn3gn9B0XiOY3XFjN2HU4Z1OqQDoksP979Ch8yH0_U6U-D8=}
MODE = k3s
PIPELINE_URL = http://127.0.0.1:8080
[Prod Cluster (k8s)]
ARTIFACTS_DIR = ${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 =
STATUS = Built
[Initialization Scripts]
; No configuration values captured yet for this section.
[Deployment]
MODE = k3s
TARGET = prole-service-cluster
[Install]
; No configuration values captured yet for this section.

View File

@ -1,12 +0,0 @@
# Port forward configuration for Prole services (generated).
# Format: id: local=<hostPort> remote=<servicePort> ns=<namespace> svc=<service> address=<bind>
argocd: local=8081 remote=80 ns=argocd svc=argocd-server address=0.0.0.0
garage: local=3900 remote=3900 ns=default svc=garage address=0.0.0.0
openbao: local=8200 remote=8200 ns=default svc=openbao address=127.0.0.1
opentofu: local=8080 remote=8080 ns=default 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=prole-db-a0001 svc=prole-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
prole-db-manager: local=80 remote=80 ns=prole-db-a0001 svc=prole-db-manager address=0.0.0.0

View File

@ -1,3 +1,11 @@
# Port mappings for Prole Tools (generated). # Port mappings for Prole Tools (generated).
# Format: key: local=... remote=... ns=... svc=... address=... # 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=default svc=garage address=0.0.0.0
openbao: local=8200 remote=8200 ns=default svc=openbao address=127.0.0.1
opentofu: local=8080 remote=8080 ns=default 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=prole-db001 svc=prole-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,132 +0,0 @@
# PostgreSQL Client Authentication Configuration File
# ===================================================
#
# Refer to the "Client Authentication" section in the PostgreSQL
# documentation for a complete description of this file. A short
# synopsis follows.
#
# ----------------------
# Authentication Records
# ----------------------
#
# This file controls: which hosts are allowed to connect, how clients
# are authenticated, which PostgreSQL user names they can use, which
# databases they can access. Records take one of these forms:
#
# local DATABASE USER METHOD [OPTIONS]
# host DATABASE USER ADDRESS METHOD [OPTIONS]
# hostssl DATABASE USER ADDRESS METHOD [OPTIONS]
# hostnossl DATABASE USER ADDRESS METHOD [OPTIONS]
# hostgssenc DATABASE USER ADDRESS METHOD [OPTIONS]
# hostnogssenc DATABASE USER ADDRESS METHOD [OPTIONS]
#
# (The uppercase items must be replaced by actual values.)
#
# The first field is the connection type:
# - "local" is a Unix-domain socket
# - "host" is a TCP/IP socket (encrypted or not)
# - "hostssl" is a TCP/IP socket that is SSL-encrypted
# - "hostnossl" is a TCP/IP socket that is not SSL-encrypted
# - "hostgssenc" is a TCP/IP socket that is GSSAPI-encrypted
# - "hostnogssenc" is a TCP/IP socket that is not GSSAPI-encrypted
#
# DATABASE can be "all", "sameuser", "samerole", "replication", a
# database name, a regular expression (if it starts with a slash (/))
# or a comma-separated list thereof. The "all" keyword does not match
# "replication". Access to replication must be enabled in a separate
# record (see example below).
#
# USER can be "all", a user name, a group name prefixed with "+", a
# regular expression (if it starts with a slash (/)) or a comma-separated
# list thereof. In both the DATABASE and USER fields you can also write
# a file name prefixed with "@" to include names from a separate file.
#
# ADDRESS specifies the set of hosts the record matches. It can be a
# host name, or it is made up of an IP address and a CIDR mask that is
# an integer (between 0 and 32 (IPv4) or 128 (IPv6) inclusive) that
# specifies the number of significant bits in the mask. A host name
# that starts with a dot (.) matches a suffix of the actual host name.
# Alternatively, you can write an IP address and netmask in separate
# columns to specify the set of hosts. Instead of a CIDR-address, you
# can write "samehost" to match any of the server's own IP addresses,
# or "samenet" to match any address in any subnet that the server is
# directly connected to.
#
# METHOD can be "trust", "reject", "md5", "password", "scram-sha-256",
# "gss", "sspi", "ident", "peer", "pam", "ldap", "radius" or "cert".
# Note that "password" sends passwords in clear text; "md5" or
# "scram-sha-256" are preferred since they send encrypted passwords.
#
# OPTIONS are a set of options for the authentication in the format
# NAME=VALUE. The available options depend on the different
# authentication methods -- refer to the "Client Authentication"
# section in the documentation for a list of which options are
# available for which authentication methods.
#
# Database and user names containing spaces, commas, quotes and other
# special characters must be quoted. Quoting one of the keywords
# "all", "sameuser", "samerole" or "replication" makes the name lose
# its special character, and just match a database or username with
# that name.
#
# ---------------
# Include Records
# ---------------
#
# This file allows the inclusion of external files or directories holding
# more records, using the following keywords:
#
# include FILE
# include_if_exists FILE
# include_dir DIRECTORY
#
# FILE is the file name to include, and DIR is the directory name containing
# the file(s) to include. Any file in a directory will be loaded if suffixed
# with ".conf". The files of a directory are ordered by name.
# include_if_exists ignores missing files. FILE and DIRECTORY can be
# specified as a relative or an absolute path, and can be double-quoted if
# they contain spaces.
#
# -------------
# Miscellaneous
# -------------
#
# This file is read on server startup and when the server receives a
# SIGHUP signal. If you edit the file on a running system, you have to
# SIGHUP the server for the changes to take effect, run "pg_ctl reload",
# or execute "SELECT pg_reload_conf()".
#
# ----------------------------------
# Put your actual configuration here
# ----------------------------------
#
# If you want to allow non-local connections, you need to add more
# "host" records. In that case you will also need to make PostgreSQL
# listen on a non-local interface via the listen_addresses
# configuration parameter, or via the -i or -h command line switches.
# DO NOT DISABLE!
# If you change this first entry you will need to make sure that the
# database superuser can access the database using some other method.
# Noninteractive access to all databases is required during automatic
# maintenance (custom daily cronjobs, replication, and similar tasks).
#
# Database administrative login by Unix domain socket
local all postgres peer
# TYPE DATABASE USER ADDRESS METHOD
# "local" is for Unix domain socket connections only
local all all peer
# IPv4 local connections:
host all all 127.0.0.1/32 scram-sha-256
# IPv6 local connections:
host all all ::1/128 scram-sha-256
# Allow replication connections from localhost, by a user with the
# replication privilege.
local replication all peer
host replication all 127.0.0.1/32 scram-sha-256
host replication all ::1/128 scram-sha-256

View File

@ -1,72 +0,0 @@
# PostgreSQL User Name Maps
# =========================
#
# ---------------
# Mapping Records
# ---------------
#
# Refer to the PostgreSQL documentation, chapter "Client
# Authentication" for a complete description. A short synopsis
# follows.
#
# This file controls PostgreSQL user name mapping. It maps external
# user names to their corresponding PostgreSQL user names. Records
# are of the form:
#
# MAPNAME SYSTEM-USERNAME PG-USERNAME
#
# (The uppercase quantities must be replaced by actual values.)
#
# MAPNAME is the (otherwise freely chosen) map name that was used in
# pg_hba.conf. SYSTEM-USERNAME is the detected user name of the
# client. PG-USERNAME is the requested PostgreSQL user name. The
# existence of a record specifies that SYSTEM-USERNAME may connect as
# PG-USERNAME.
#
# If SYSTEM-USERNAME starts with a slash (/), it will be treated as a
# regular expression. Optionally this can contain a capture (a
# parenthesized subexpression). The substring matching the capture
# will be substituted for \1 (backslash-one) if present in
# PG-USERNAME.
#
# PG-USERNAME can be "all", a user name, a group name prefixed with "+", or
# a regular expression (if it starts with a slash (/)). If it is a regular
# expression, the substring matching with \1 has no effect.
#
# Multiple maps may be specified in this file and used by pg_hba.conf.
#
# No map names are defined in the default configuration. If all
# system user names and PostgreSQL user names are the same, you don't
# need anything in this file.
#
# ---------------
# Include Records
# ---------------
#
# This file allows the inclusion of external files or directories holding
# more records, using the following keywords:
#
# include FILE
# include_if_exists FILE
# include_dir DIRECTORY
#
# FILE is the file name to include, and DIR is the directory name containing
# the file(s) to include. Any file in a directory will be loaded if suffixed
# with ".conf". The files of a directory are ordered by name.
# include_if_exists ignores missing files. FILE and DIRECTORY can be
# specified as a relative or an absolute path, and can be double-quoted if
# they contain spaces.
#
# -------------------------------
# Miscellaneous
# -------------------------------
#
# This file is read on server startup and when the postmaster receives
# a SIGHUP signal. If you edit the file on a running system, you have
# to SIGHUP the postmaster for the changes to take effect. You can
# use "pg_ctl reload" to do that.
# Put your actual configuration here
# ----------------------------------
# MAPNAME SYSTEM-USERNAME PG-USERNAME

View File

@ -1,844 +0,0 @@
# -----------------------------
# PostgreSQL configuration file
# -----------------------------
#
# This file consists of lines of the form:
#
# name = value
#
# (The "=" is optional.) Whitespace may be used. Comments are introduced with
# "#" anywhere on a line. The complete list of parameter names and allowed
# values can be found in the PostgreSQL documentation.
#
# The commented-out settings shown in this file represent the default values.
# Re-commenting a setting is NOT sufficient to revert it to the default value;
# you need to reload the server.
#
# This file is read on server startup and when the server receives a SIGHUP
# signal. If you edit the file on a running system, you have to SIGHUP the
# server for the changes to take effect, run "pg_ctl reload", or execute
# "SELECT pg_reload_conf()". Some parameters, which are marked below,
# require a server shutdown and restart to take effect.
#
# Any parameter can also be given as a command-line option to the server, e.g.,
# "postgres -c log_connections=on". Some parameters can be changed at run time
# with the "SET" SQL command.
#
# Memory units: B = bytes Time units: us = microseconds
# kB = kilobytes ms = milliseconds
# MB = megabytes s = seconds
# GB = gigabytes min = minutes
# TB = terabytes h = hours
# d = days
#------------------------------------------------------------------------------
# FILE LOCATIONS
#------------------------------------------------------------------------------
# The default values of these variables are driven from the -D command-line
# option or PGDATA environment variable, represented here as ConfigDir.
data_directory = '/var/lib/postgresql/17/main' # use data in another directory
# (change requires restart)
hba_file = '/etc/postgresql/17/main/pg_hba.conf' # host-based authentication file
# (change requires restart)
ident_file = '/etc/postgresql/17/main/pg_ident.conf' # ident configuration file
# (change requires restart)
# If external_pid_file is not explicitly set, no extra PID file is written.
external_pid_file = '/var/run/postgresql/17-main.pid' # write an extra PID file
# (change requires restart)
#------------------------------------------------------------------------------
# CONNECTIONS AND AUTHENTICATION
#------------------------------------------------------------------------------
# - Connection Settings -
#listen_addresses = 'localhost' # what IP address(es) to listen on;
# comma-separated list of addresses;
# defaults to 'localhost'; use '*' for all
# (change requires restart)
port = 5432 # (change requires restart)
max_connections = 100 # (change requires restart)
#reserved_connections = 0 # (change requires restart)
#superuser_reserved_connections = 3 # (change requires restart)
unix_socket_directories = '/var/run/postgresql' # comma-separated list of directories
# (change requires restart)
#unix_socket_group = '' # (change requires restart)
#unix_socket_permissions = 0777 # begin with 0 to use octal notation
# (change requires restart)
#bonjour = off # advertise server via Bonjour
# (change requires restart)
#bonjour_name = '' # defaults to the computer name
# (change requires restart)
# - TCP settings -
# see "man tcp" for details
#tcp_keepalives_idle = 0 # TCP_KEEPIDLE, in seconds;
# 0 selects the system default
#tcp_keepalives_interval = 0 # TCP_KEEPINTVL, in seconds;
# 0 selects the system default
#tcp_keepalives_count = 0 # TCP_KEEPCNT;
# 0 selects the system default
#tcp_user_timeout = 0 # TCP_USER_TIMEOUT, in milliseconds;
# 0 selects the system default
#client_connection_check_interval = 0 # time between checks for client
# disconnection while running queries;
# 0 for never
# - Authentication -
#authentication_timeout = 1min # 1s-600s
#password_encryption = scram-sha-256 # scram-sha-256 or md5
#scram_iterations = 4096
# GSSAPI using Kerberos
#krb_server_keyfile = 'FILE:${sysconfdir}/krb5.keytab'
#krb_caseins_users = off
#gss_accept_delegation = off
# - SSL -
ssl = on
#ssl_ca_file = ''
ssl_cert_file = '/etc/ssl/certs/ssl-cert-snakeoil.pem'
#ssl_crl_file = ''
#ssl_crl_dir = ''
ssl_key_file = '/etc/ssl/private/ssl-cert-snakeoil.key'
#ssl_ciphers = 'HIGH:MEDIUM:+3DES:!aNULL' # allowed SSL ciphers
#ssl_prefer_server_ciphers = on
#ssl_ecdh_curve = 'prime256v1'
#ssl_min_protocol_version = 'TLSv1.2'
#ssl_max_protocol_version = ''
#ssl_dh_params_file = ''
#ssl_passphrase_command = ''
#ssl_passphrase_command_supports_reload = off
#------------------------------------------------------------------------------
# RESOURCE USAGE (except WAL)
#------------------------------------------------------------------------------
# - Memory -
shared_buffers = 128MB # min 128kB
# (change requires restart)
#huge_pages = try # on, off, or try
# (change requires restart)
#huge_page_size = 0 # zero for system default
# (change requires restart)
#temp_buffers = 8MB # min 800kB
#max_prepared_transactions = 0 # zero disables the feature
# (change requires restart)
# Caution: it is not advisable to set max_prepared_transactions nonzero unless
# you actively intend to use prepared transactions.
#work_mem = 4MB # min 64kB
#hash_mem_multiplier = 2.0 # 1-1000.0 multiplier on hash table work_mem
#maintenance_work_mem = 64MB # min 64kB
#autovacuum_work_mem = -1 # min 64kB, or -1 to use maintenance_work_mem
#logical_decoding_work_mem = 64MB # min 64kB
#max_stack_depth = 2MB # min 100kB
#shared_memory_type = mmap # the default is the first option
# supported by the operating system:
# mmap
# sysv
# windows
# (change requires restart)
dynamic_shared_memory_type = posix # the default is usually the first option
# supported by the operating system:
# posix
# sysv
# windows
# mmap
# (change requires restart)
#min_dynamic_shared_memory = 0MB # (change requires restart)
#vacuum_buffer_usage_limit = 2MB # size of vacuum and analyze buffer access strategy ring;
# 0 to disable vacuum buffer access strategy;
# range 128kB to 16GB
# SLRU buffers (change requires restart)
#commit_timestamp_buffers = 0 # memory for pg_commit_ts (0 = auto)
#multixact_offset_buffers = 16 # memory for pg_multixact/offsets
#multixact_member_buffers = 32 # memory for pg_multixact/members
#notify_buffers = 16 # memory for pg_notify
#serializable_buffers = 32 # memory for pg_serial
#subtransaction_buffers = 0 # memory for pg_subtrans (0 = auto)
#transaction_buffers = 0 # memory for pg_xact (0 = auto)
# - Disk -
#temp_file_limit = -1 # limits per-process temp file space
# in kilobytes, or -1 for no limit
#max_notify_queue_pages = 1048576 # limits the number of SLRU pages allocated
# for NOTIFY / LISTEN queue
# - Kernel Resources -
#max_files_per_process = 1000 # min 64
# (change requires restart)
# - Cost-Based Vacuum Delay -
#vacuum_cost_delay = 0 # 0-100 milliseconds (0 disables)
#vacuum_cost_page_hit = 1 # 0-10000 credits
#vacuum_cost_page_miss = 2 # 0-10000 credits
#vacuum_cost_page_dirty = 20 # 0-10000 credits
#vacuum_cost_limit = 200 # 1-10000 credits
# - Background Writer -
#bgwriter_delay = 200ms # 10-10000ms between rounds
#bgwriter_lru_maxpages = 100 # max buffers written/round, 0 disables
#bgwriter_lru_multiplier = 2.0 # 0-10.0 multiplier on buffers scanned/round
#bgwriter_flush_after = 512kB # measured in pages, 0 disables
# - Asynchronous Behavior -
#backend_flush_after = 0 # measured in pages, 0 disables
#effective_io_concurrency = 1 # 1-1000; 0 disables prefetching
#maintenance_io_concurrency = 10 # 1-1000; 0 disables prefetching
#io_combine_limit = 128kB # usually 1-32 blocks (depends on OS)
#max_worker_processes = 8 # (change requires restart)
#max_parallel_workers_per_gather = 2 # limited by max_parallel_workers
#max_parallel_maintenance_workers = 2 # limited by max_parallel_workers
#max_parallel_workers = 8 # number of max_worker_processes that
# can be used in parallel operations
#parallel_leader_participation = on
#------------------------------------------------------------------------------
# WRITE-AHEAD LOG
#------------------------------------------------------------------------------
# - Settings -
#wal_level = replica # minimal, replica, or logical
# (change requires restart)
#fsync = on # flush data to disk for crash safety
# (turning this off can cause
# unrecoverable data corruption)
#synchronous_commit = on # synchronization level;
# off, local, remote_write, remote_apply, or on
#wal_sync_method = fsync # the default is the first option
# supported by the operating system:
# open_datasync
# fdatasync (default on Linux and FreeBSD)
# fsync
# fsync_writethrough
# open_sync
#full_page_writes = on # recover from partial page writes
#wal_log_hints = off # also do full page writes of non-critical updates
# (change requires restart)
#wal_compression = off # enables compression of full-page writes;
# off, pglz, lz4, zstd, or on
#wal_init_zero = on # zero-fill new WAL files
#wal_recycle = on # recycle WAL files
#wal_buffers = -1 # min 32kB, -1 sets based on shared_buffers
# (change requires restart)
#wal_writer_delay = 200ms # 1-10000 milliseconds
#wal_writer_flush_after = 1MB # measured in pages, 0 disables
#wal_skip_threshold = 2MB
#commit_delay = 0 # range 0-100000, in microseconds
#commit_siblings = 5 # range 1-1000
# - Checkpoints -
#checkpoint_timeout = 5min # range 30s-1d
#checkpoint_completion_target = 0.9 # checkpoint target duration, 0.0 - 1.0
#checkpoint_flush_after = 256kB # measured in pages, 0 disables
#checkpoint_warning = 30s # 0 disables
max_wal_size = 1GB
min_wal_size = 80MB
# - Prefetching during recovery -
#recovery_prefetch = try # prefetch pages referenced in the WAL?
#wal_decode_buffer_size = 512kB # lookahead window used for prefetching
# (change requires restart)
# - Archiving -
#archive_mode = off # enables archiving; off, on, or always
# (change requires restart)
#archive_library = '' # library to use to archive a WAL file
# (empty string indicates archive_command should
# be used)
#archive_command = '' # command to use to archive a WAL file
# placeholders: %p = path of file to archive
# %f = file name only
# e.g. 'test ! -f /mnt/server/archivedir/%f && cp %p /mnt/server/archivedir/%f'
#archive_timeout = 0 # force a WAL file switch after this
# number of seconds; 0 disables
# - Archive Recovery -
# These are only used in recovery mode.
#restore_command = '' # command to use to restore an archived WAL file
# placeholders: %p = path of file to restore
# %f = file name only
# e.g. 'cp /mnt/server/archivedir/%f %p'
#archive_cleanup_command = '' # command to execute at every restartpoint
#recovery_end_command = '' # command to execute at completion of recovery
# - Recovery Target -
# Set these only when performing a targeted recovery.
#recovery_target = '' # 'immediate' to end recovery as soon as a
# consistent state is reached
# (change requires restart)
#recovery_target_name = '' # the named restore point to which recovery will proceed
# (change requires restart)
#recovery_target_time = '' # the time stamp up to which recovery will proceed
# (change requires restart)
#recovery_target_xid = '' # the transaction ID up to which recovery will proceed
# (change requires restart)
#recovery_target_lsn = '' # the WAL LSN up to which recovery will proceed
# (change requires restart)
#recovery_target_inclusive = on # Specifies whether to stop:
# just after the specified recovery target (on)
# just before the recovery target (off)
# (change requires restart)
#recovery_target_timeline = 'latest' # 'current', 'latest', or timeline ID
# (change requires restart)
#recovery_target_action = 'pause' # 'pause', 'promote', 'shutdown'
# (change requires restart)
# - WAL Summarization -
#summarize_wal = off # run WAL summarizer process?
#wal_summary_keep_time = '10d' # when to remove old summary files, 0 = never
#------------------------------------------------------------------------------
# REPLICATION
#------------------------------------------------------------------------------
# - Sending Servers -
# Set these on the primary and on any standby that will send replication data.
#max_wal_senders = 10 # max number of walsender processes
# (change requires restart)
#max_replication_slots = 10 # max number of replication slots
# (change requires restart)
#wal_keep_size = 0 # in megabytes; 0 disables
#max_slot_wal_keep_size = -1 # in megabytes; -1 disables
#wal_sender_timeout = 60s # in milliseconds; 0 disables
#track_commit_timestamp = off # collect timestamp of transaction commit
# (change requires restart)
# - Primary Server -
# These settings are ignored on a standby server.
#synchronous_standby_names = '' # standby servers that provide sync rep
# method to choose sync standbys, number of sync standbys,
# and comma-separated list of application_name
# from standby(s); '*' = all
#synchronized_standby_slots = '' # streaming replication standby server slot
# names that logical walsender processes will wait for
# - Standby Servers -
# These settings are ignored on a primary server.
#primary_conninfo = '' # connection string to sending server
#primary_slot_name = '' # replication slot on sending server
#hot_standby = on # "off" disallows queries during recovery
# (change requires restart)
#max_standby_archive_delay = 30s # max delay before canceling queries
# when reading WAL from archive;
# -1 allows indefinite delay
#max_standby_streaming_delay = 30s # max delay before canceling queries
# when reading streaming WAL;
# -1 allows indefinite delay
#wal_receiver_create_temp_slot = off # create temp slot if primary_slot_name
# is not set
#wal_receiver_status_interval = 10s # send replies at least this often
# 0 disables
#hot_standby_feedback = off # send info from standby to prevent
# query conflicts
#wal_receiver_timeout = 60s # time that receiver waits for
# communication from primary
# in milliseconds; 0 disables
#wal_retrieve_retry_interval = 5s # time to wait before retrying to
# retrieve WAL after a failed attempt
#recovery_min_apply_delay = 0 # minimum delay for applying changes during recovery
#sync_replication_slots = off # enables slot synchronization on the physical standby from the primary
# - Subscribers -
# These settings are ignored on a publisher.
#max_logical_replication_workers = 4 # taken from max_worker_processes
# (change requires restart)
#max_sync_workers_per_subscription = 2 # taken from max_logical_replication_workers
#max_parallel_apply_workers_per_subscription = 2 # taken from max_logical_replication_workers
#------------------------------------------------------------------------------
# QUERY TUNING
#------------------------------------------------------------------------------
# - Planner Method Configuration -
#enable_async_append = on
#enable_bitmapscan = on
#enable_gathermerge = on
#enable_hashagg = on
#enable_hashjoin = on
#enable_incremental_sort = on
#enable_indexscan = on
#enable_indexonlyscan = on
#enable_material = on
#enable_memoize = on
#enable_mergejoin = on
#enable_nestloop = on
#enable_parallel_append = on
#enable_parallel_hash = on
#enable_partition_pruning = on
#enable_partitionwise_join = off
#enable_partitionwise_aggregate = off
#enable_presorted_aggregate = on
#enable_seqscan = on
#enable_sort = on
#enable_tidscan = on
#enable_group_by_reordering = on
# - Planner Cost Constants -
#seq_page_cost = 1.0 # measured on an arbitrary scale
#random_page_cost = 4.0 # same scale as above
#cpu_tuple_cost = 0.01 # same scale as above
#cpu_index_tuple_cost = 0.005 # same scale as above
#cpu_operator_cost = 0.0025 # same scale as above
#parallel_setup_cost = 1000.0 # same scale as above
#parallel_tuple_cost = 0.1 # same scale as above
#min_parallel_table_scan_size = 8MB
#min_parallel_index_scan_size = 512kB
#effective_cache_size = 4GB
#jit_above_cost = 100000 # perform JIT compilation if available
# and query more expensive than this;
# -1 disables
#jit_inline_above_cost = 500000 # inline small functions if query is
# more expensive than this; -1 disables
#jit_optimize_above_cost = 500000 # use expensive JIT optimizations if
# query is more expensive than this;
# -1 disables
# - Genetic Query Optimizer -
#geqo = on
#geqo_threshold = 12
#geqo_effort = 5 # range 1-10
#geqo_pool_size = 0 # selects default based on effort
#geqo_generations = 0 # selects default based on effort
#geqo_selection_bias = 2.0 # range 1.5-2.0
#geqo_seed = 0.0 # range 0.0-1.0
# - Other Planner Options -
#default_statistics_target = 100 # range 1-10000
#constraint_exclusion = partition # on, off, or partition
#cursor_tuple_fraction = 0.1 # range 0.0-1.0
#from_collapse_limit = 8
#jit = on # allow JIT compilation
#join_collapse_limit = 8 # 1 disables collapsing of explicit
# JOIN clauses
#plan_cache_mode = auto # auto, force_generic_plan or
# force_custom_plan
#recursive_worktable_factor = 10.0 # range 0.001-1000000
#------------------------------------------------------------------------------
# REPORTING AND LOGGING
#------------------------------------------------------------------------------
# - Where to Log -
#log_destination = 'stderr' # Valid values are combinations of
# stderr, csvlog, jsonlog, syslog, and
# eventlog, depending on platform.
# csvlog and jsonlog require
# logging_collector to be on.
# This is used when logging to stderr:
#logging_collector = off # Enable capturing of stderr, jsonlog,
# and csvlog into log files. Required
# to be on for csvlogs and jsonlogs.
# (change requires restart)
# These are only used if logging_collector is on:
#log_directory = 'log' # directory where log files are written,
# can be absolute or relative to PGDATA
#log_filename = 'postgresql-%Y-%m-%d_%H%M%S.log' # log file name pattern,
# can include strftime() escapes
#log_file_mode = 0600 # creation mode for log files,
# begin with 0 to use octal notation
#log_rotation_age = 1d # Automatic rotation of logfiles will
# happen after that time. 0 disables.
#log_rotation_size = 10MB # Automatic rotation of logfiles will
# happen after that much log output.
# 0 disables.
#log_truncate_on_rotation = off # If on, an existing log file with the
# same name as the new log file will be
# truncated rather than appended to.
# But such truncation only occurs on
# time-driven rotation, not on restarts
# or size-driven rotation. Default is
# off, meaning append to existing files
# in all cases.
# These are relevant when logging to syslog:
#syslog_facility = 'LOCAL0'
#syslog_ident = 'postgres'
#syslog_sequence_numbers = on
#syslog_split_messages = on
# This is only relevant when logging to eventlog (Windows):
# (change requires restart)
#event_source = 'PostgreSQL'
# - When to Log -
#log_min_messages = warning # values in order of decreasing detail:
# debug5
# debug4
# debug3
# debug2
# debug1
# info
# notice
# warning
# error
# log
# fatal
# panic
#log_min_error_statement = error # values in order of decreasing detail:
# debug5
# debug4
# debug3
# debug2
# debug1
# info
# notice
# warning
# error
# log
# fatal
# panic (effectively off)
#log_min_duration_statement = -1 # -1 is disabled, 0 logs all statements
# and their durations, > 0 logs only
# statements running at least this number
# of milliseconds
#log_min_duration_sample = -1 # -1 is disabled, 0 logs a sample of statements
# and their durations, > 0 logs only a sample of
# statements running at least this number
# of milliseconds;
# sample fraction is determined by log_statement_sample_rate
#log_statement_sample_rate = 1.0 # fraction of logged statements exceeding
# log_min_duration_sample to be logged;
# 1.0 logs all such statements, 0.0 never logs
#log_transaction_sample_rate = 0.0 # fraction of transactions whose statements
# are logged regardless of their duration; 1.0 logs all
# statements from all transactions, 0.0 never logs
#log_startup_progress_interval = 10s # Time between progress updates for
# long-running startup operations.
# 0 disables the feature, > 0 indicates
# the interval in milliseconds.
# - What to Log -
#debug_print_parse = off
#debug_print_rewritten = off
#debug_print_plan = off
#debug_pretty_print = on
#log_autovacuum_min_duration = 10min # log autovacuum activity;
# -1 disables, 0 logs all actions and
# their durations, > 0 logs only
# actions running at least this number
# of milliseconds.
#log_checkpoints = on
#log_connections = off
#log_disconnections = off
#log_duration = off
#log_error_verbosity = default # terse, default, or verbose messages
#log_hostname = off
log_line_prefix = '%m [%p] %q%u@%d ' # special values:
# %a = application name
# %u = user name
# %d = database name
# %r = remote host and port
# %h = remote host
# %b = backend type
# %p = process ID
# %P = process ID of parallel group leader
# %t = timestamp without milliseconds
# %m = timestamp with milliseconds
# %n = timestamp with milliseconds (as a Unix epoch)
# %Q = query ID (0 if none or not computed)
# %i = command tag
# %e = SQL state
# %c = session ID
# %l = session line number
# %s = session start timestamp
# %v = virtual transaction ID
# %x = transaction ID (0 if none)
# %q = stop here in non-session
# processes
# %% = '%'
# e.g. '<%u%%%d> '
#log_lock_waits = off # log lock waits >= deadlock_timeout
#log_recovery_conflict_waits = off # log standby recovery conflict waits
# >= deadlock_timeout
#log_parameter_max_length = -1 # when logging statements, limit logged
# bind-parameter values to N bytes;
# -1 means print in full, 0 disables
#log_parameter_max_length_on_error = 0 # when logging an error, limit logged
# bind-parameter values to N bytes;
# -1 means print in full, 0 disables
#log_statement = 'none' # none, ddl, mod, all
#log_replication_commands = off
#log_temp_files = -1 # log temporary files equal or larger
# than the specified size in kilobytes;
# -1 disables, 0 logs all temp files
log_timezone = 'Etc/UTC'
# - Process Title -
cluster_name = '17/main' # added to process titles if nonempty
# (change requires restart)
#update_process_title = on
#------------------------------------------------------------------------------
# STATISTICS
#------------------------------------------------------------------------------
# - Cumulative Query and Index Statistics -
#track_activities = on
#track_activity_query_size = 1024 # (change requires restart)
#track_counts = on
#track_io_timing = off
#track_wal_io_timing = off
#track_functions = none # none, pl, all
#stats_fetch_consistency = cache # cache, none, snapshot
# - Monitoring -
#compute_query_id = auto
#log_statement_stats = off
#log_parser_stats = off
#log_planner_stats = off
#log_executor_stats = off
#------------------------------------------------------------------------------
# AUTOVACUUM
#------------------------------------------------------------------------------
#autovacuum = on # Enable autovacuum subprocess? 'on'
# requires track_counts to also be on.
#autovacuum_max_workers = 3 # max number of autovacuum subprocesses
# (change requires restart)
#autovacuum_naptime = 1min # time between autovacuum runs
#autovacuum_vacuum_threshold = 50 # min number of row updates before
# vacuum
#autovacuum_vacuum_insert_threshold = 1000 # min number of row inserts
# before vacuum; -1 disables insert
# vacuums
#autovacuum_analyze_threshold = 50 # min number of row updates before
# analyze
#autovacuum_vacuum_scale_factor = 0.2 # fraction of table size before vacuum
#autovacuum_vacuum_insert_scale_factor = 0.2 # fraction of inserts over table
# size before insert vacuum
#autovacuum_analyze_scale_factor = 0.1 # fraction of table size before analyze
#autovacuum_freeze_max_age = 200000000 # maximum XID age before forced vacuum
# (change requires restart)
#autovacuum_multixact_freeze_max_age = 400000000 # maximum multixact age
# before forced vacuum
# (change requires restart)
#autovacuum_vacuum_cost_delay = 2ms # default vacuum cost delay for
# autovacuum, in milliseconds;
# -1 means use vacuum_cost_delay
#autovacuum_vacuum_cost_limit = -1 # default vacuum cost limit for
# autovacuum, -1 means use
# vacuum_cost_limit
#------------------------------------------------------------------------------
# CLIENT CONNECTION DEFAULTS
#------------------------------------------------------------------------------
# - Statement Behavior -
#client_min_messages = notice # values in order of decreasing detail:
# debug5
# debug4
# debug3
# debug2
# debug1
# log
# notice
# warning
# error
#search_path = '"$user", public' # schema names
#row_security = on
#default_table_access_method = 'heap'
#default_tablespace = '' # a tablespace name, '' uses the default
#default_toast_compression = 'pglz' # 'pglz' or 'lz4'
#temp_tablespaces = '' # a list of tablespace names, '' uses
# only default tablespace
#check_function_bodies = on
#default_transaction_isolation = 'read committed'
#default_transaction_read_only = off
#default_transaction_deferrable = off
#session_replication_role = 'origin'
#statement_timeout = 0 # in milliseconds, 0 is disabled
#transaction_timeout = 0 # in milliseconds, 0 is disabled
#lock_timeout = 0 # in milliseconds, 0 is disabled
#idle_in_transaction_session_timeout = 0 # in milliseconds, 0 is disabled
#idle_session_timeout = 0 # in milliseconds, 0 is disabled
#vacuum_freeze_table_age = 150000000
#vacuum_freeze_min_age = 50000000
#vacuum_failsafe_age = 1600000000
#vacuum_multixact_freeze_table_age = 150000000
#vacuum_multixact_freeze_min_age = 5000000
#vacuum_multixact_failsafe_age = 1600000000
#bytea_output = 'hex' # hex, escape
#xmlbinary = 'base64'
#xmloption = 'content'
#gin_pending_list_limit = 4MB
#createrole_self_grant = '' # set and/or inherit
#event_triggers = on
# - Locale and Formatting -
datestyle = 'iso, mdy'
#intervalstyle = 'postgres'
timezone = 'Etc/UTC'
#timezone_abbreviations = 'Default' # Select the set of available time zone
# abbreviations. Currently, there are
# Default
# Australia (historical usage)
# India
# You can create your own file in
# share/timezonesets/.
#extra_float_digits = 1 # min -15, max 3; any value >0 actually
# selects precise output mode
#client_encoding = sql_ascii # actually, defaults to database
# encoding
# These settings are initialized by initdb, but they can be changed.
lc_messages = 'C.UTF-8' # locale for system error message
# strings
lc_monetary = 'C.UTF-8' # locale for monetary formatting
lc_numeric = 'C.UTF-8' # locale for number formatting
lc_time = 'C.UTF-8' # locale for time formatting
#icu_validation_level = warning # report ICU locale validation
# errors at the given level
# default configuration for text search
default_text_search_config = 'pg_catalog.english'
# - Shared Library Preloading -
#local_preload_libraries = ''
#session_preload_libraries = ''
#shared_preload_libraries = '' # (change requires restart)
#jit_provider = 'llvmjit' # JIT library to use
# - Other Defaults -
#dynamic_library_path = '$libdir'
#extension_destdir = '' # prepend path when loading extensions
# and shared objects (added by Debian)
#gin_fuzzy_search_limit = 0
#------------------------------------------------------------------------------
# LOCK MANAGEMENT
#------------------------------------------------------------------------------
#deadlock_timeout = 1s
#max_locks_per_transaction = 64 # min 10
# (change requires restart)
#max_pred_locks_per_transaction = 64 # min 10
# (change requires restart)
#max_pred_locks_per_relation = -2 # negative values mean
# (max_pred_locks_per_transaction
# / -max_pred_locks_per_relation) - 1
#max_pred_locks_per_page = 2 # min 0
#------------------------------------------------------------------------------
# VERSION AND PLATFORM COMPATIBILITY
#------------------------------------------------------------------------------
# - Previous PostgreSQL Versions -
#array_nulls = on
#backslash_quote = safe_encoding # on, off, or safe_encoding
#escape_string_warning = on
#lo_compat_privileges = off
#quote_all_identifiers = off
#standard_conforming_strings = on
#synchronize_seqscans = on
# - Other Platforms and Clients -
#transform_null_equals = off
#allow_alter_system = on
#------------------------------------------------------------------------------
# ERROR HANDLING
#------------------------------------------------------------------------------
#exit_on_error = off # terminate session on any error?
#restart_after_crash = on # reinitialize after backend crash?
#data_sync_retry = off # retry or panic on failure to fsync
# data?
# (change requires restart)
#recovery_init_sync_method = fsync # fsync, syncfs (Linux 5.8+)
#------------------------------------------------------------------------------
# CONFIG FILE INCLUDES
#------------------------------------------------------------------------------
# These options allow settings to be loaded from files other than the
# default postgresql.conf. Note that these are directives, not variable
# assignments, so they can usefully be given more than once.
include_dir = 'conf.d' # include files ending in '.conf' from
# a directory, e.g., 'conf.d'
#include_if_exists = '...' # include file only if it exists
#include = '...' # include file
#------------------------------------------------------------------------------
# CUSTOMIZED OPTIONS
#------------------------------------------------------------------------------
# Add settings for extensions here

View File

@ -1,21 +1,36 @@
; Prole Master Configuration File ; Prole Master Configuration File
; Generated by install.py on 2026-03-01 00:30:19 ; Generated by install.py on 2026-03-01 19:22:14
; This file is used as input for Ansible deployment and k8s cluster creation. ; This file is used as input for Ansible deployment and k8s cluster creation.
[User] [User]
; User-editable values; derived values below reference these by default. ; User-editable values; derived values below reference these by default.
NAMESPACE = prole-db-a0001 NAMESPACE = prole-db001
PROLE_CONF = ${PROLE_HOME}/conf PROLE_CONF = /Users/chrisfu/dev/prole/conf
PROLE_DATA = /Users/chrisfu/.prole/data PROLE_DATA = /Users/chrisfu/.prole/data
PROLE_HOME = /Users/chrisfu/dev/prole PROLE_HOME = mock_val
PROLE_LOGS = /opt/prole/logs/chrisfu PROLE_LOGS = /opt/prole/logs/chrisfu
PROLE_SERVICE = ${PROLE_HOME}/etc PROLE_SERVICE = /Users/chrisfu/dev/prole/etc
SERVICE_NAMESPACE = default SERVICE_NAMESPACE = default
[Inputs] [Inputs]
; Screen-scoped inputs used for unattended replays (-S) ; Screen-scoped inputs used for unattended replays (-S)
build.deploy_env = Dev build.deploy_env = Dev
build.run_build = false build.run_build = false
database_options.distribution = percona
database_options.ext.dblink = True
database_options.ext.pg_buffercache = True
database_options.ext.pg_freespacemap = True
database_options.ext.pg_repack = True
database_options.ext.pg_stat_monitor = True
database_options.ext.pg_stat_statements = True
database_options.ext.pgaudit = True
database_options.ext.pgbadger = True
database_options.ext.pgcrypto = True
database_options.ext.pgrowlocks = True
database_options.ext.pgvector = True
database_options.ext.postgis = True
database_options.ext.postgres_fdw = True
database_options.version_type = v18
dependencies.ansible.install = true dependencies.ansible.install = true
dependencies.auto_install_missing = true dependencies.auto_install_missing = true
dependencies.brew.install = true dependencies.brew.install = true
@ -27,40 +42,41 @@ dependencies.opentofu.install = true
dependencies.python.install = true dependencies.python.install = true
dependencies.verify_all = false dependencies.verify_all = false
disk_selection.disk_type = local disk_selection.disk_type = local
disk_selection.local_path = ${PROLE_HOME}/prole-tools-app/dist disk_selection.local_path = /Users/chrisfu
disk_selection.removable_mount = disk_selection.removable_mount =
env_setup.NAMESPACE = prole-hq0-db0 env_setup.NAMESPACE = ${NAMESPACE}
env_setup.PROLE_CONF = ${PROLE_CONF} env_setup.PROLE_CONF = ${PROLE_CONF}
env_setup.PROLE_DATA = ${PROLE_DATA} env_setup.PROLE_DATA = ${PROLE_DATA}
env_setup.PROLE_HOME = ${PROLE_HOME} env_setup.PROLE_HOME = ${PROLE_HOME}
env_setup.PROLE_LOGS = ${PROLE_LOGS} env_setup.PROLE_LOGS = ${PROLE_LOGS}
env_setup.PROLE_SERVICE = ${PROLE_SERVICE} env_setup.PROLE_SERVICE = ${PROLE_SERVICE}
init_cluster.at_rest_encryption_enabled = true init_cluster.at_rest_encryption_enabled = true
init_cluster.cluster_env = service init_cluster.cluster_env = dev
init_cluster.deployment_target = prole-service-cluster init_cluster.deployment_target = prole-dev-cluster
init_cluster.gitops_enabled = false init_cluster.gitops_enabled = false
init_cluster.k3s_server_url = https://myrddin.prole.org:6443 init_cluster.k3s_server_url =
init_cluster.k3s_token = ${OPENBAO:REDACTED} init_cluster.k3s_token =
init_cluster.kerberos_enabled = true init_cluster.kerberos_enabled = false
init_cluster.mode = k3s init_cluster.mode = k3d
init_cluster.start_cluster = true init_cluster.start_cluster = true
init_cluster.supabase_enabled = false init_cluster.supabase_enabled = false
init_cnpg_deploy.force_rollout = false init_cnpg_deploy.force_rollout = false
init_cnpg_deploy.run_deploy = true init_cnpg_deploy.run_deploy = true
init_db_build.run_build = true init_db_build.run_build = true
init_password.db_host_port = 5432 init_password.db_host_port = 5432
init_password.db_namespace = prole-hq0-db0 init_password.db_namespace = ${NAMESPACE}
init_password.db_password = ${OPENBAO:kv/prole/${NAMESPACE}/db#password} init_password.db_password = ${OPENBAO:kv/prole/prole-db001/db#password}
init_password.db_password_confirm = ${OPENBAO:kv/prole/${NAMESPACE}/db#password} init_password.db_password_confirm = ${OPENBAO:kv/prole/prole-db001/db#password}
init_password.db_username = chrisfu init_password.db_username = root
init_password.generate_ssh_key = true init_password.generate_ssh_key = true
init_scripts.run_scripts = true init_scripts.run_scripts = true
kerberos_config.enabled = true kerberos_config.enabled = False
kerberos_config.kdc = 10.0.0.3 kerberos_config.init_authority = false
kerberos_config.password = ${OPENBAO:kv/prole/${NAMESPACE}/kerberos#password} kerberos_config.kdc = 10.0.0.205
kerberos_config.realm = PROLE.ORG kerberos_config.password = ${OPENBAO:kv/prole/prole-db001/kerberos#password}
kerberos_config.test_connection = true kerberos_config.realm =
kerberos_config.user = chrisfu kerberos_config.test_connection = false
kerberos_config.user = administrator
network_scan.run = true network_scan.run = true
ollama_config.model = ollama_config.model =
ollama_config.server_host = ollama_config.server_host =
@ -68,41 +84,53 @@ ollama_config.server_port = 11434
[Global] [Global]
; Variables used by name in more than one place or assumed global scope ; Variables used by name in more than one place or assumed global scope
CLUSTER_ENV = service ARGOCD_NAMESPACE = argocd
CLUSTER_ENV = dev
DB_HOST_PORT = 5432 DB_HOST_PORT = 5432
DB_PASSWORD = ${OPENBAO:kv/prole/${NAMESPACE}/db#password} DB_PASSWORD = ${OPENBAO:kv/prole/prole-db001/db#password}
DEPLOYMENT_MODE = k3s DEPLOYMENT_MODE = k3d
DEPLOYMENT_TARGET = prole-service-cluster DEPLOYMENT_TARGET = knoe-dev-cluster
DOCKER_IMPORT_DIR =
NAMESPACE = ${NAMESPACE} NAMESPACE = ${NAMESPACE}
PROLE_DB_USER = chrisfu PROLE_DB_USER = root
PROLE_HOME = ${PROLE_HOME} PROLE_HOME = ${PROLE_HOME}
PROLE_K3S_SERVER = https://myrddin.prole.org:6443 PROLE_K3S_SERVER =
PROLE_K3S_TOKEN = ${OPENBAO:REDACTED} PROLE_K3S_TOKEN = ${OPENBAO:kv/prole/prole-db001/k3s#token}
PROLE_OPENTOFU_URL = http://127.0.0.1:8080 PROLE_OPENTOFU_URL = http://127.0.0.1:8080
REGISTRY_NAMESPACE = default
SERVICE_NAMESPACE = default SERVICE_NAMESPACE = default
[Welcome] [Welcome]
; No configuration values captured yet for this section. ; No configuration values captured yet for this section.
[Dependencies] [Dependencies]
; No configuration values captured yet for this section. STATUS = All installed
[Network] [Network]
AD_DC_HOST = myrddin.prole.org AD_DC_HOST = myrddin.prole.org
AD_DC_IP = 10.0.0.3 AD_DC_IP = 10.0.0.3
ANSIBLE_DOMAIN = prole.org ANSIBLE_DOMAIN = prole.org
ANSIBLE_INFRASTRUCTURE = ${PROLE_HOME}/infrastructure ANSIBLE_INFRASTRUCTURE = /Users/chrisfu/dev/prole/infrastructure
ANSIBLE_INVENTORY = ${PROLE_HOME}/infrastructure/inventory ANSIBLE_INVENTORY = /Users/chrisfu/dev/prole/infrastructure/inventory
ANSIBLE_REALM = PROLE.ORG ANSIBLE_REALM = PROLE.ORG
ANSIBLE_TOPOLOGY = {"domain":"prole.org","realm":"PROLE.ORG","internal_records":{"aventage.prole.org":"10.0.0.206","fairyland.prole.org":"10.0.0.208","loghost.prole.org":"10.0.0.3","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"],"pihole":["pi.prole.org","raspberry.prole.org"],"ad_dc":["myrddin.prole.org"],"k3s_hosts":["myrddin.prole.org","pi.prole.org","retropie.prole.org"],"linux_hosts":["pi.prole.org","raspberry.prole.org","myrddin.prole.org","retropie.prole.org"]},"hosts":{"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":[]} 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","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"],"pihole":["pi.prole.org","raspberry.prole.org"],"ad_dc":["myrddin.prole.org"],"k3s_hosts":["myrddin.prole.org","pi.prole.org","retropie.prole.org"],"linux_hosts":["pi.prole.org","raspberry.prole.org","myrddin.prole.org","retropie.prole.org"]},"hosts":{"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":[]}
KDC_ANSIBLE_DETECTED = 10.0.0.3 KDC_ANSIBLE_DETECTED = 10.0.0.3
KDC_AUTO_DETECTED = 10.0.0.3 KDC_AUTO_DETECTED = 10.0.0.205
KERBEROS_AUTO_ENABLED = True KERBEROS_AUTO_ENABLED = False
[Port Forwards] [Port Forwards]
; No configuration values captured yet for this section. PORT_FORWARD_K3D_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_K3D_MAPPING_2 = id=garage;namespace=default;target=svc/garage;address=0.0.0.0;hostPort=3900;servicePort=3900;protocol=TCP;description=Garage S3
PORT_FORWARD_K3D_MAPPING_3 = id=openbao;namespace=default;target=svc/openbao;address=127.0.0.1;hostPort=8200;servicePort=8200;protocol=TCP;description=OpenBao
PORT_FORWARD_K3D_MAPPING_4 = id=opentofu;namespace=default;target=svc/opentofu;address=0.0.0.0;hostPort=8080;servicePort=8080;protocol=TCP;description=OpenTofu
PORT_FORWARD_K3D_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_K3D_MAPPING_6 = id=postgres;namespace=${NAMESPACE};target=svc/prole-db-rw;address=0.0.0.0;hostPort=5432;servicePort=5432;protocol=TCP;description=PostgreSQL (primary)
PORT_FORWARD_K3D_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_K3D_MAPPING_8 = id=grafana;namespace=monitoring;target=svc/kps-grafana;address=0.0.0.0;hostPort=3000;servicePort=80;protocol=TCP;description=Grafana UI
STATUS = Running
[System Environment] [System Environment]
NAMESPACE = prole-db001
PROLE_CONF = ${PROLE_CONF} PROLE_CONF = ${PROLE_CONF}
PROLE_DATA = ${PROLE_DATA} PROLE_DATA = ${PROLE_DATA}
PROLE_HOME = ${PROLE_HOME} PROLE_HOME = ${PROLE_HOME}
@ -113,70 +141,59 @@ PROLE_SERVICE = ${PROLE_SERVICE}
; No configuration values captured yet for this section. ; No configuration values captured yet for this section.
[Kerberos Authentication] [Kerberos Authentication]
AD_PORT_FORWARD = 1 PASSWORD = ${OPENBAO:kv/prole/prole-db001/kerberos#password}
AD_PROXY_HOST_NETWORK = 1 STATUS = Skipped
AD_PROXY_IMAGE = alpine/socat
AD_PROXY_SERVICE = prole-kerberos-ad-dc
AD_TCP_PORTS = 88 389 445 464 636
AD_UDP_PORTS = 88 464
ENABLED = True
KDC = 10.0.0.3
PASSWORD = ${OPENBAO:kv/prole/${NAMESPACE}/kerberos#password}
REALM = PROLE.ORG
SERVER = 10.0.0.3
USER = chrisfu
[Ollama] [Ollama]
; No configuration values captured yet for this section. ; No configuration values captured yet for this section.
[Optional Features] [Optional Features]
AT_REST_ENCRYPTION_ENABLED = True AT_REST_ENCRYPTION_ENABLED = true
GITOPS_ENABLED = False GITOPS_ENABLED = false
KERBEROS_ENABLED = True KERBEROS_ENABLED = false
SUPABASE_ENABLED = False SUPABASE_ENABLED = false
[GitOps] [GitOps]
; No configuration values captured yet for this section. STATUS = Skipped
[Database Creation] [Database Creation]
DB_NAME = prole-db001
DB_USER = root
NAMESPACE = ${NAMESPACE} NAMESPACE = ${NAMESPACE}
[Initialize Cluster] [Initialize Cluster]
ENVIRONMENT = service ; No configuration values captured yet for this section.
K3S_SERVER_URL = https://myrddin.prole.org:6443
K3S_TOKEN = ${OPENBAO:REDACTED}
[Dev Cluster (k3d)] [Dev Cluster (k3d)]
CLUSTER_ENV = dev CLUSTER_ENV = k3d-knoe-dev-cluster
DISPLAY_NAME = prole-dev-cluster DISPLAY_NAME = knoe-dev-cluster
KUBECTL_CONTEXT = prole-k3s KUBECTL_CONTEXT = dev
MODE = k3d MODE = k3d
[Service Cluster (k3s)] [Service Cluster (k3s)]
CLUSTER_ENV = prole-service-cluster CLUSTER_ENV = prole-service-cluster
DISPLAY_NAME = prole-service-cluster DISPLAY_NAME = prole-service-cluster
K3S_SERVER_URL = https://myrddin.prole.org:6443 K3S_SERVER_URL =
K3S_TOKEN = ${OPENBAO:REDACTED} K3S_TOKEN = ${OPENBAO:kv/prole/prole-db001/k3s#token}
MODE = k3s MODE = k3s
PIPELINE_URL = http://127.0.0.1:8080 PIPELINE_URL = http://127.0.0.1:8080
[Prod Cluster (k8s)] [Prod Cluster (k8s)]
ARTIFACTS_DIR = /Users/chrisfu/dev/prole/data/staging ARTIFACTS_DIR = ${PROLE_DATA}/staging
CLUSTER_ENV = prole-prod-cluster CLUSTER_ENV = prole-prod-cluster
DISPLAY_NAME = prole-prod-cluster DISPLAY_NAME = prole-prod-cluster
MODE = k8s MODE = k8s
PIPELINE_URL = http://127.0.0.1:8080 PIPELINE_URL = http://127.0.0.1:8080
[Docker Build] [Docker Build]
LOCAL_REGISTRY = localhost:5000 ; No configuration values captured yet for this section.
LOCAL_REGISTRY_INTERNAL = k3d-prole-registry.localhost:5000
[Initialization Scripts] [Initialization Scripts]
; No configuration values captured yet for this section. ; No configuration values captured yet for this section.
[Deployment] [Deployment]
MODE = k3s MODE = k3d
TARGET = prole-service-cluster TARGET = prole-dev-cluster
[Install] [Install]
; No configuration values captured yet for this section. STATUS = Finished

View File

@ -1,175 +0,0 @@
; Prole Master Configuration File
; Generated by install.py on 2026-02-05 15:29:20
; This file is used as input for Ansible deployment and k8s cluster creation.
[User]
; User-editable values; derived values below reference these by default.
PROLE_HOME = /Users/chrisfu/dev/prole
PROLE_CONF = ${PROLE_HOME}/conf
PROLE_SERVICE = ${PROLE_HOME}/etc
PROLE_DATA = /opt/prole/data/chrisfu
PROLE_LOGS = /opt/prole/logs/chrisfu
NAMESPACE = ${NAMESPACE}
SERVICE_NAMESPACE = default
[Inputs]
; Screen-scoped inputs used for unattended replays (-S)
build.deploy_env = Dev
build.run_build = false
dependencies.auto_install_missing = true
dependencies.brew.install = true
dependencies.docker.install = true
dependencies.k3d.install = true
dependencies.verify_all = false
disk_selection.disk_type = local
disk_selection.local_path = ${PROLE_HOME}/prole-tools-app/dist
disk_selection.removable_mount =
env_setup.NAMESPACE = ${NAMESPACE}
env_setup.PROLE_CONF = ${PROLE_CONF}
env_setup.PROLE_DATA = ${PROLE_DATA}
env_setup.PROLE_HOME = ${PROLE_HOME}
env_setup.PROLE_LOGS = ${PROLE_LOGS}
env_setup.PROLE_SERVICE = ${PROLE_SERVICE}
init_cluster.at_rest_encryption_enabled = true
init_cluster.cluster_env = prole-service-cluster
init_cluster.k3s_server_url = https://pi.prole.org:6443
init_cluster.k3s_token = ${PROLE_SECRET:v1:fsX0MfG2qqQpKoPi:JqND7OComMcPDaRCpw6iiXL8w9If-70rvmbQA2V6lz9hK2MpPTX796dp-r3-qiIQYMc-sax9OobVoZLXdfBbzMmkR270uzl_CeDNuVBWnPmXcd00DYnBTsfs8hcGvJ6EuswizHsNVcIBxIUftPuyXr2ZQQVcC8VBuRlphw==}
init_cluster.kerberos_enabled = false
init_cluster.start_cluster = true
init_cluster.supabase_enabled = false
init_cnpg_deploy.force_rollout = false
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 = ${OPENBAO:kv/prole/prole-chrisfu-abracadabra/db#password}
init_password.db_password_confirm = ${OPENBAO:kv/prole/prole-chrisfu-abracadabra/db#password}
init_password.db_username = chrisfu
init_password.generate_ssh_key = true
init_scripts.run_scripts = true
kerberos_config.enabled = false
kerberos_config.kdc = 10.0.0.3
kerberos_config.password = ${OPENBAO:kv/prole/prole-chrisfu-abracadabra/kerberos#password}
kerberos_config.realm = PROLE.ORG
kerberos_config.test_connection = false
kerberos_config.user =
network_scan.run = true
[Global]
; Variables used by name in more than one place or assumed global scope
CLUSTER_ENV = prole-service-cluster
DB_HOST_PORT = 5432
DB_PASSWORD = ${OPENBAO:kv/prole/prole-chrisfu-abracadabra/db#password}
DEPLOYMENT_MODE = k3s
DEPLOYMENT_TARGET = prole-service-cluster
DOCKER_IMPORT_DIR =
NAMESPACE = ${NAMESPACE}
PROLE_DB_USER = chrisfu
PROLE_HOME = ${PROLE_HOME}
PROLE_K3S_SERVER = https://pi.prole.org:6443
PROLE_K3S_TOKEN = ${PROLE_SECRET:v1:EpUBQqBLpN0hKPcy:0MdKwHBxLbrY71XZSsbvhHSudotpC_hCmyO98HfDG08pCDzCzFiVzHzuO_LoRclwViNEipmqyl0JjwPQsVEaIBt5B1AjGeVcU1iFoDKvYf5pl2bAjTWIXKtNg8eq6LFTMGoe-I72bFcwuRJ17ykzoCaXdwC6c7Ru_UZxwQ==}
PROLE_OPENTOFU_URL = http://127.0.0.1:8080
SERVICE_NAMESPACE = default
ARGOCD_NAMESPACE = argocd
REGISTRY_NAMESPACE = default
PROLE_ENABLE_LOCAL_REGISTRY = true
[Welcome]
; No configuration values captured yet for this section.
[Dependencies]
STATUS = All installed
[Network]
AD_DC_HOST = myrddin.prole.org
AD_DC_IP = 10.0.0.3
ANSIBLE_DOMAIN = prole.org
ANSIBLE_INFRASTRUCTURE = ${PROLE_HOME}/infrastructure
ANSIBLE_INVENTORY = ${PROLE_HOME}/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","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://pi.prole.org:6443","server_host":"pi.prole.org","token_present":true},"groups":{"iscsi":["pi.prole.org","raspberry.prole.org","myrddin.prole.org","retropie.prole.org"],"pihole":["pi.prole.org","raspberry.prole.org"],"ad_dc":["myrddin.prole.org"],"k3s_hosts":["pi.prole.org","myrddin.prole.org","retropie.prole.org"],"linux_hosts":["pi.prole.org","raspberry.prole.org","myrddin.prole.org","retropie.prole.org"]},"hosts":{"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":[]}
KDC_ANSIBLE_DETECTED = 10.0.0.3
[Port Forwards]
; Port-forward defaults for local k3d and external k3s access.
PORT_FORWARD_K3D_ENABLED = true
PORT_FORWARD_K3S_ENABLED = true
PORT_FORWARD_EXTERNAL_DB_HOST = myrddin.prole.org
PORT_FORWARD_EXTERNAL_DB_IP = 10.0.0.3
PORT_FORWARD_EXTERNAL_DB_PORT = 5432
PORT_FORWARD_EXTERNAL_DB_SERVICE = prole-db-rw
; Mapping format: id=...;namespace=...;target=...;address=...;hostPort=...;servicePort=...;protocol=TCP;description=...
PORT_FORWARD_K3D_MAPPING_1 = 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 (https://127.0.0.1:8443)
PORT_FORWARD_K3D_MAPPING_2 = 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 (http://127.0.0.1:9090)
PORT_FORWARD_K3D_MAPPING_3 = id=grafana;namespace=monitoring;target=svc/kps-grafana;address=0.0.0.0;hostPort=3000;servicePort=3000;protocol=TCP;description=Grafana UI (0.0.0.0:3000)
PORT_FORWARD_K3D_MAPPING_4 = id=postgres;namespace=${NAMESPACE};target=svc/prole-db-rw;address=0.0.0.0;hostPort=5432;servicePort=5432;protocol=TCP;description=PostgreSQL (primary) (0.0.0.0:5432)
PORT_FORWARD_K3D_MAPPING_5 = id=openbao;namespace=${NAMESPACE};target=svc/openbao;address=127.0.0.1;hostPort=8200;servicePort=8200;protocol=TCP;description=OpenBao UI (http://127.0.0.1:8200)
PORT_FORWARD_K3S_MAPPING_1 = id=grafana;namespace=monitoring;target=svc/kps-grafana;address=0.0.0.0;hostPort=3000;servicePort=3000;protocol=TCP;description=Grafana UI (0.0.0.0:3000)
PORT_FORWARD_K3S_MAPPING_2 = id=opentofu;namespace=${NAMESPACE};target=svc/opentofu;address=0.0.0.0;hostPort=8080;servicePort=8080;protocol=TCP;description=OpenTofu UI/API (0.0.0.0:8080)
PORT_FORWARD_K3S_MAPPING_3 = id=openbao;namespace=${NAMESPACE};target=svc/openbao;address=0.0.0.0;hostPort=8200;servicePort=8200;protocol=TCP;description=OpenVault/OpenBao UI/API (0.0.0.0:8200)
PORT_FORWARD_K3S_MAPPING_4 = id=postgres;namespace=${NAMESPACE};target=svc/prole-db-rw;address=0.0.0.0;hostPort=5432;servicePort=5432;protocol=TCP;description=PostgreSQL external (0.0.0.0:5432)
[System Environment]
PROLE_CONF = ${PROLE_CONF}
PROLE_DATA = ${PROLE_DATA}
PROLE_HOME = ${PROLE_HOME}
PROLE_LOGS = ${PROLE_LOGS}
PROLE_SERVICE = ${PROLE_SERVICE}
PROLE_OPENTOFU_URL = http://127.0.0.1:8080
[Monitoring]
GRAFANA_ADMIN_PASSWORD = ${OPENBAO:kv/prole/prole-chrisfu-abracadabra/monitoring#grafana_admin_password}
[Kerberos Authentication]
PASSWORD = ${OPENBAO:kv/prole/prole-chrisfu-abracadabra/kerberos#password}
[Optional Features]
AT_REST_ENCRYPTION_ENABLED = true
KERBEROS_ENABLED = false
SUPABASE_ENABLED = false
[Database Creation]
DB_NAME = prole-chrisfu-abracadabra
DB_PASSWORD_SET = true
DB_USER = chrisfu
NAMESPACE = prole-chrisfu-abracadabra
[Initialize Cluster]
ENVIRONMENT = prole-service-cluster
K3S_SERVER_URL = https://pi.prole.org:6443
K3S_TOKEN = ${PROLE_SECRET:v1:87PgULFcPofp_fez:sWTKxPhs0ykJEtWLlAj_cWFGK5pe1ttmeQhOfSnPfNLDe8L6qL7U5mv6OQfbT9n1z46zoJ9QKaYCCvwjIgdTjOkdjE9YGrHs1IhkSVGbnFZ531LCZ0eoj_ouhutPYcFpC5b7L990zbcwMYGyZ8Cr-ohIMRk7mN-Xp_PB4w==}
[Dev Cluster (k3d)]
CLUSTER_ENV = k3d-prole-dev-cluster
DISPLAY_NAME = prole-dev-cluster
KUBECTL_CONTEXT = prole-service-cluster
MODE = k3d
[Service Cluster (k3s)]
CLUSTER_ENV = prole-service-cluster
DISPLAY_NAME = prole-service-cluster
K3S_SERVER_URL = https://pi.prole.org:6443
K3S_TOKEN = ${PROLE_SECRET:v1:gLFC-kFjv_0LzEO7:fTdBIU1aFqYM1f2eILrkzYCay1A0ITp_1cfHMYlX47The7ritLEV8R3_dxKgx3L34kCw0dptoqfQL65VoLTbBLN55pus7CQs-3bb-XCJLJ0VUDPfavI9yEJ7u3VJw4SYvSGIrRa6U4YTAnFCcICMly7g6XfFnOsGoz4W2w==}
MODE = k3s
PIPELINE_URL = http://127.0.0.1:8080
[Prod Cluster (k8s)]
ARTIFACTS_DIR = ${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 =
[Initialization Scripts]
; No configuration values captured yet for this section.
[Deployment]
MODE = k3s
TARGET = prole-service-cluster
[Install]
STATUS = Failed

View File

@ -1,89 +0,0 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: prole-db-manager
labels:
app: prole-db-manager
spec:
replicas: 1
selector:
matchLabels:
app: prole-db-manager
template:
metadata:
labels:
app: prole-db-manager
spec:
serviceAccountName: prole-db-manager
containers:
- name: db-manager
image: prole-db-manager:latest
imagePullPolicy: IfNotPresent
ports:
- containerPort: 80
name: http
env:
- name: PORT
value: "80"
- name: NAMESPACE
valueFrom:
fieldRef:
fieldPath: metadata.namespace
- name: BARMAN_PLUGIN_NAME
value: "barman-cloud.cloudnative-pg.io"
- name: BARMAN_OBJECT_NAME
value: "prole-db-barman-objectstore"
readinessProbe:
httpGet:
path: /health
port: http
initialDelaySeconds: 3
periodSeconds: 10
livenessProbe:
httpGet:
path: /health
port: http
initialDelaySeconds: 5
periodSeconds: 30
resources:
requests:
cpu: 50m
memory: 64Mi
limits:
cpu: 200m
memory: 128Mi
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: prole-db-manager
labels:
app: prole-db-manager
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: prole-db-manager
labels:
app: prole-db-manager
rules:
- apiGroups: ["postgresql.cnpg.io"]
resources: ["backups"]
verbs: ["create", "get", "list", "watch"]
- apiGroups: ["postgresql.cnpg.io"]
resources: ["clusters"]
verbs: ["get", "list"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: prole-db-manager
labels:
app: prole-db-manager
subjects:
- kind: ServiceAccount
name: prole-db-manager
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: prole-db-manager

View File

@ -1,14 +0,0 @@
apiVersion: v1
kind: Service
metadata:
name: prole-db-manager
labels:
app: prole-db-manager
spec:
selector:
app: prole-db-manager
ports:
- name: http
port: 80
targetPort: http
type: ClusterIP

2
env.sh
View File

@ -8,7 +8,7 @@ export PROLE_CONF="/Users/chrisfu/dev/prole/conf"
export PROLE_DATA="/Users/chrisfu/.prole/data" export PROLE_DATA="/Users/chrisfu/.prole/data"
export PROLE_LOGS="/opt/prole/logs/chrisfu" export PROLE_LOGS="/opt/prole/logs/chrisfu"
export PROLE_SERVICE="/Users/chrisfu/dev/prole/etc" export PROLE_SERVICE="/Users/chrisfu/dev/prole/etc"
export NAMESPACE="prole-db-a0001" export NAMESPACE="prole-db001"
# Ensure PATH works for GUI-launched shells (Docker, etc.) # Ensure PATH works for GUI-launched shells (Docker, etc.)
_prole_add_path() { case ":${PATH}:" in *":$1:"*) ;; *) PATH="$1:${PATH:-}" ;; esac; } _prole_add_path() { case ":${PATH}:" in *":$1:"*) ;; *) PATH="$1:${PATH:-}" ;; esac; }

2
etc/home.sh Executable file
View File

@ -0,0 +1,2 @@
#!/bin/bash
echo hometest

View File

@ -47,7 +47,9 @@ if [[ -n "${COMMON_CORE_PARSE_ERROR:-}" ]]; then
fi fi
ACTION="$COMMON_CORE_ACTION" ACTION="$COMMON_CORE_ACTION"
CERTMGR_NAMESPACE="$(common_core_resolve_namespace "default")" # cert-manager always runs in its own dedicated namespace; never follows the
# common-services namespace override.
CERTMGR_NAMESPACE="cert-manager"
common_core_apply_namespace "$CERTMGR_NAMESPACE" common_core_apply_namespace "$CERTMGR_NAMESPACE"
CERTMGR_VERSION="${CERTMGR_VERSION:-v1.14.6}" CERTMGR_VERSION="${CERTMGR_VERSION:-v1.14.6}"
@ -84,38 +86,16 @@ ensure_namespace() {
fi fi
} }
_patch_manifest_namespace() { _apply_manifest() {
local ns="$1" curl -fsSL "$CERTMGR_RELEASE_URL" | kubectl apply -f -
if [[ "$ns" == "cert-manager" ]]; then
cat
return 0
fi
awk -v ns="$ns" '
$1=="kind:" && $2=="Namespace" {in_ns=1}
in_ns && $1=="name:" && $2=="cert-manager" {
match($0, /^[[:space:]]*/);
indent = substr($0, RSTART, RLENGTH);
print indent "name: " ns;
in_ns=0;
next
}
$1=="namespace:" && $2=="cert-manager" {
match($0, /^[[:space:]]*/);
indent = substr($0, RSTART, RLENGTH);
print indent "namespace: " ns;
next
}
{print}
'
} }
_apply_manifest() { _is_already_deployed() {
curl -fsSL "$CERTMGR_RELEASE_URL" | _patch_manifest_namespace "$CERTMGR_NAMESPACE" | kubectl apply -f - kubectl -n "$CERTMGR_NAMESPACE" get deployment cert-manager cert-manager-cainjector cert-manager-webhook >/dev/null 2>&1
} }
_delete_manifest() { _delete_manifest() {
curl -fsSL "$CERTMGR_RELEASE_URL" | _patch_manifest_namespace "$CERTMGR_NAMESPACE" | kubectl delete -f - --ignore-not-found=true curl -fsSL "$CERTMGR_RELEASE_URL" | kubectl delete -f - --ignore-not-found=true
} }
wait_rollout() { wait_rollout() {
@ -143,8 +123,12 @@ restart() {
action_update() { action_update() {
ensure_tools ensure_tools
ensure_namespace ensure_namespace
_apply_manifest if _is_already_deployed; then
wait_rollout echo "cert-manager already deployed in namespace '$CERTMGR_NAMESPACE'; skipping manifest apply."
else
_apply_manifest
wait_rollout
fi
prole_register_port_forward "certmgr" "$CERTMGR_NAMESPACE" "svc/cert-manager-webhook" "9443" "9443" "${CERTMGR_PF_ADDR}" "TCP" "CertManager Webhook" prole_register_port_forward "certmgr" "$CERTMGR_NAMESPACE" "svc/cert-manager-webhook" "9443" "9443" "${CERTMGR_PF_ADDR}" "TCP" "CertManager Webhook"
} }

View File

@ -577,6 +577,137 @@ apply_prole_manifest_file() {
return 1 return 1
} }
# Push image to local registry (via LOCAL_REGISTRY host address) with k3d import fallback.
_push_to_k3d_registry() {
local image="$1"
local cluster_name="$2"
local push_host="${LOCAL_REGISTRY:-localhost:5000}"
local plain_image="${image##*/}" # strip registry prefix, e.g. prole-db:18-088
if [[ -n "$push_host" ]]; then
local push_ref="${push_host}/${plain_image}"
docker tag "$image" "$push_ref" 2>/dev/null || true
if docker push "$push_ref" 2>/dev/null; then
echo " ✓ Image pushed to registry at '${push_host}'."
return 0
fi
echo " WARN: push to '${push_host}' failed; falling back to k3d image import ..." >&2
fi
if k3d image import "$image" -c "$cluster_name" 2>/dev/null; then
echo " ✓ Image '$image' imported directly into k3d cluster '$cluster_name'."
return 0
fi
echo "ERROR: Failed to push or import image '$image'." >&2
return 1
}
# Pre-flight: ensure the prole-db image is available in the k3d cluster before
# the CNPG operator ever tries to pull it, avoiding ErrImagePull backoff loops.
# Steps: containerd cache → Docker daemon (registry tag) → Docker daemon (plain tag)
# → tar import → docker build + push/import.
_ensure_prole_db_image() {
[[ "${PROLE_MODE:-}" != "k3d" ]] && return 0
local image_override="${CNPG_IMAGE:-${PROLE_DB_IMAGE:-}}"
local image=""
if [[ -n "$image_override" ]]; then
image="$image_override"
else
if [[ "$VERSION" == "latest" || -z "$VERSION" ]]; then
image=$(get_latest_image)
else
image="prole-db:$VERSION"
fi
fi
image=$(resolve_cnpg_image "$image")
# Auto-detect active k3d cluster name
local cluster_name="${K3D_CLUSTER_NAME:-}"
if [[ -z "$cluster_name" ]]; then
cluster_name=$(k3d cluster list --no-headers 2>/dev/null | awk '{print $1}' | head -1)
fi
cluster_name="${cluster_name:-knoe-dev-cluster}"
local prole_db_dir="${PROLE_HOME:-$SCRIPT_DIR/..}/prole-db"
local plain_image="${image##*/}" # e.g. prole-db:18-088
echo "Pre-flight: verifying image '$image' is available in k3d cluster '$cluster_name' ..."
# Step 1: check if already present in k3d containerd with matching digest
local containerd_digest local_digest containerd_sha
containerd_digest=$(docker exec "k3d-${cluster_name}-server-0" \
ctr images ls -q 2>/dev/null | grep -F "$image" | head -1 || true)
if [[ -n "$containerd_digest" ]]; then
local_digest=$(docker inspect --format='{{index .RepoDigests 0}}' "$image" 2>/dev/null \
| awk -F@ '{print $2}' || true)
containerd_sha=$(docker exec "k3d-${cluster_name}-server-0" \
ctr images ls 2>/dev/null | grep -F "$image" | awk '{print $3}' | head -1 || true)
if [[ -z "$local_digest" || "$containerd_sha" == "$local_digest" ]]; then
echo " ✓ Image '$image' already in k3d containerd (digest match); no import needed."
return 0
fi
echo " Image '$image' in containerd but digest mismatch (local: ${local_digest:-unknown}, containerd: ${containerd_sha:-unknown}); re-importing ..."
docker exec "k3d-${cluster_name}-server-0" ctr images rm "$image" 2>/dev/null || true
fi
# Step 2a: registry-tagged image in local Docker daemon → push + import
if docker image inspect "$image" >/dev/null 2>&1; then
echo " Image '$image' found in Docker daemon; pushing to registry ..."
_push_to_k3d_registry "$image" "$cluster_name"
return $?
fi
# Step 2b: plain-tagged image in local Docker daemon → tag + push + import
if docker image inspect "$plain_image" >/dev/null 2>&1; then
echo " Plain image '$plain_image' found in Docker daemon; tagging as '$image' and pushing ..."
docker tag "$plain_image" "$image"
_push_to_k3d_registry "$image" "$cluster_name"
return $?
fi
# Step 3: look for a matching tar in the docker-import directory
local prole_data="${PROLE_DATA:-$HOME/.prole/data}"
local docker_import_dir="${DOCKER_IMPORT_DIR:-${prole_data}/docker-import}"
local name_part="${plain_image%%:*}" # e.g. prole-db
local tag_part="${plain_image##*:}" # e.g. 18-088
local found_tar=""
if [[ -d "$docker_import_dir" ]]; then
for _t in "$docker_import_dir"/*.tar; do
[[ -f "$_t" ]] || continue
local _bn
_bn=$(basename "$_t" .tar)
if [[ "$_bn" == *"$name_part"* && "$_bn" == *"$tag_part"* ]]; then
found_tar="$_t"
break
fi
done
fi
if [[ -n "$found_tar" ]]; then
echo " Loading tar '$(basename "$found_tar")' into Docker daemon ..."
docker load -i "$found_tar"
docker tag "$plain_image" "$image" 2>/dev/null || true
_push_to_k3d_registry "$image" "$cluster_name"
return $?
fi
# Step 4: image not found anywhere — build from source then push + import
if [[ ! -f "$prole_db_dir/Dockerfile" ]]; then
echo "ERROR: Dockerfile not found in '$prole_db_dir'; cannot build prole-db image." >&2
return 1
fi
echo " Image '$image' not found in k3d, Docker daemon, or docker-import dir."
echo " Building prole-db image from '$prole_db_dir' ..."
if ! docker build -t "$plain_image" "$prole_db_dir"; then
echo "ERROR: docker build failed for image '$plain_image'." >&2
return 1
fi
docker tag "$plain_image" "$image"
echo " Build complete. Pushing '$image' to registry ..."
_push_to_k3d_registry "$image" "$cluster_name"
return $?
}
ensure_prole_stack_resources() { ensure_prole_stack_resources() {
echo "Applying CloudNative-PG cluster and related resources ..." echo "Applying CloudNative-PG cluster and related resources ..."
local image_override="${CNPG_IMAGE:-${PROLE_DB_IMAGE:-}}" local image_override="${CNPG_IMAGE:-${PROLE_DB_IMAGE:-}}"
@ -726,6 +857,7 @@ wait_for_cnpg_pods() {
echo " Timeout: ${timeout}s" echo " Timeout: ${timeout}s"
local last_feedback=0 local last_feedback=0
local feedback_interval=30 # print detailed status every 30s local feedback_interval=30 # print detailed status every 30s
local _image_pull_remediated="" # set after first k3d import attempt to avoid loops
while true; do while true; do
local now elapsed local now elapsed
now=$(date +%s) now=$(date +%s)
@ -735,13 +867,47 @@ wait_for_cnpg_pods() {
pods=$(kubectl -n "$NAMESPACE" get pods -l "cnpg.io/cluster=$CNPG_CLUSTER_NAME" --no-headers 2>/dev/null || true) pods=$(kubectl -n "$NAMESPACE" get pods -l "cnpg.io/cluster=$CNPG_CLUSTER_NAME" --no-headers 2>/dev/null || true)
if [[ -n "$pods" ]]; then if [[ -n "$pods" ]]; then
# Check for Error or CrashLoopBackOff # Check for Error or CrashLoopBackOff (terminal failures)
if echo "$pods" | grep -E "Error|CrashLoopBackOff" >/dev/null; then if echo "$pods" | grep -E "Error|CrashLoopBackOff" >/dev/null; then
echo "ERROR: Some CNPG pods are in Error or CrashLoopBackOff state:" >&2 echo "ERROR: Some CNPG pods are in Error or CrashLoopBackOff state:" >&2
echo "$pods" | grep -E "Error|CrashLoopBackOff" >&2 echo "$pods" | grep -E "Error|CrashLoopBackOff" >&2
return 1 return 1
fi fi
# Check for image pull failures — attempt remediation in k3d mode
if echo "$pods" | grep -E "ErrImagePull|ImagePullBackOff" >/dev/null; then
if [[ "${PROLE_MODE:-}" == "k3d" ]]; then
if [[ -z "$_image_pull_remediated" ]]; then
_image_pull_remediated=1
local fail_pod fail_image
fail_pod=$(echo "$pods" | grep -E "ErrImagePull|ImagePullBackOff" | awk '{print $1}' | head -1)
# Try containers then initContainers
fail_image=$(kubectl -n "$NAMESPACE" get pod "$fail_pod" \
-o jsonpath='{.spec.containers[0].image}' 2>/dev/null || true)
if [[ -z "$fail_image" ]]; then
fail_image=$(kubectl -n "$NAMESPACE" get pod "$fail_pod" \
-o jsonpath='{.spec.initContainers[0].image}' 2>/dev/null || true)
fi
echo "WARN: Pod '$fail_pod' cannot pull image '${fail_image:-unknown}' (ErrImagePull/ImagePullBackOff in k3d mode)." >&2
import_dir="${DOCKER_IMPORT_DIR:-${PROLE_DATA:+${PROLE_DATA}/docker-import}}"
cluster_name="${K3D_CLUSTER_NAME:-}"
if [[ -z "$cluster_name" ]]; then
cluster_name=$(k3d cluster list --no-headers 2>/dev/null | awk '{print $1}' | head -1)
fi
cluster_name="${cluster_name:-knoe-dev-cluster}"
# Re-run full image ensure logic to push/import the failing image
echo " Attempting image remediation via _ensure_prole_db_image ..." >&2
_ensure_prole_db_image >&2 || true
echo " Remediation complete; resuming wait ..." >&2
fi
# Continue the wait loop — do not return 1
else
echo "ERROR: Image pull failure in non-k3d mode — cannot auto-recover:" >&2
echo "$pods" | grep -E "ErrImagePull|ImagePullBackOff" >&2
return 1
fi
fi
# Count Running pods by name (exclude initdb) # Count Running pods by name (exclude initdb)
local running_pods local running_pods
running_pods=$(kubectl -n "$NAMESPACE" get pods --no-headers 2>/dev/null \ running_pods=$(kubectl -n "$NAMESPACE" get pods --no-headers 2>/dev/null \
@ -1432,6 +1598,11 @@ initialize() {
fi fi
echo "Pre-flight check passed: all required secrets present." echo "Pre-flight check passed: all required secrets present."
if ! _ensure_prole_db_image; then
echo "ERROR: Pre-flight image check failed; aborting cluster initialization." >&2
return 1
fi
ensure_prole_stack_resources ensure_prole_stack_resources
if ! wait_for_cnpg_pods 300; then if ! wait_for_cnpg_pods 300; then
@ -1486,6 +1657,7 @@ deploy_cluster() {
generate_tls_if_missing generate_tls_if_missing
echo "Deploying $image to cluster $CNPG_CLUSTER_NAME in namespace $NAMESPACE..." echo "Deploying $image to cluster $CNPG_CLUSTER_NAME in namespace $NAMESPACE..."
_ensure_prole_db_image || true
apply_cnpg_cluster_manifest "$CNPG_MANIFEST" apply_cnpg_cluster_manifest "$CNPG_MANIFEST"
local current_image local current_image
@ -1503,6 +1675,20 @@ case "$ACTION" in
recreate) recreate)
ensure_tools ensure_tools
"$0" delete "$CNPG_CLUSTER_NAME" "$0" delete "$CNPG_CLUSTER_NAME"
# Clean up PVCs left behind by the CNPG operator (not in static manifests)
echo "Cleaning up PVCs in namespace '$NAMESPACE' ..."
kubectl -n "$NAMESPACE" delete pvc --all --ignore-not-found 2>/dev/null || true
# Wait briefly for pods to fully terminate before re-creating
echo "Waiting for pods to terminate in namespace '$NAMESPACE' ..."
_wait_term=0
while kubectl -n "$NAMESPACE" get pods --no-headers 2>/dev/null | grep -qv '^No resources'; do
sleep 3
_wait_term=$(( _wait_term + 3 ))
if (( _wait_term >= 60 )); then
echo "WARN: Pods still present after 60s; proceeding anyway." >&2
break
fi
done
"$0" create "$CNPG_CLUSTER_NAME" "$0" create "$CNPG_CLUSTER_NAME"
;; ;;
create) create)
@ -1513,7 +1699,20 @@ case "$ACTION" in
delete) delete)
ensure_tools ensure_tools
echo "Deleting all resources for '$CNPG_CLUSTER_NAME' ..." echo "Deleting all resources for '$CNPG_CLUSTER_NAME' ..."
kubectl delete -n "$NAMESPACE" -k "$SCRIPT_DIR/../k8s/prole" --ignore-not-found # Delete each manifest individually, mirroring the apply pattern:
# ingress.yaml contains multi-namespace resources and must be deleted without -n.
for _del_f in "$SCRIPT_DIR/../k8s/prole"/*.yaml; do
_del_base=$(basename "$_del_f")
case "$_del_base" in
kustomization.yaml|ingress.yaml) continue ;;
*) kubectl delete -n "$NAMESPACE" -f "$_del_f" --ignore-not-found 2>&1 \
| grep -v "^Error from server (NotFound)" || true ;;
esac
done
if [[ -f "$SCRIPT_DIR/../k8s/prole/ingress.yaml" ]]; then
kubectl delete -f "$SCRIPT_DIR/../k8s/prole/ingress.yaml" --ignore-not-found 2>&1 \
| grep -v "^Error from server (NotFound)" || true
fi
;; ;;
start) start)
ensure_tools ensure_tools

View File

@ -147,6 +147,80 @@ _resolve_prole_data() {
echo "${d:-}" echo "${d:-}"
} }
# Standard console execution element.
# Runs a command silently (output captured), displays a live elapsed-time footer
# on stderr when attached to a TTY, prints elapsed time on completion, and tails
# captured output for diagnostics on failure.
_exec_with_progress_footer() {
local label="$1"
shift
local start_time
start_time=$(date +%s)
local tmp_out
tmp_out=$(mktemp 2>/dev/null || echo "/tmp/_prole_exec_$$.out")
# Spawn live footer only when stderr is a real TTY
local spinner_pid=""
if [[ -t 2 ]]; then
(
while true; do
now=$(date +%s)
elapsed=$(( now - start_time ))
h=$(( elapsed / 3600 ))
m=$(( (elapsed % 3600) / 60 ))
s=$(( elapsed % 60 ))
printf "\r\033[2K [%02d:%02d:%02d elapsed] %s" "$h" "$m" "$s" "$label" >&2
sleep 1
done
) &
spinner_pid=$!
disown "$spinner_pid" 2>/dev/null || true
fi
"$@" >"$tmp_out" 2>&1
local rc=$?
if [[ -n "$spinner_pid" ]]; then
kill "$spinner_pid" 2>/dev/null || true
wait "$spinner_pid" 2>/dev/null || true
printf "\r\033[2K" >&2
fi
local end_time elapsed_total h m s
end_time=$(date +%s)
elapsed_total=$(( end_time - start_time ))
h=$(( elapsed_total / 3600 ))
m=$(( (elapsed_total % 3600) / 60 ))
s=$(( elapsed_total % 60 ))
printf " [%02dh:%02dm:%02ds elapsed] %s\n" "$h" "$m" "$s" "$label"
if [[ $rc -ne 0 && -s "$tmp_out" ]]; then
echo " [Last output]:" >&2
tail -n 10 "$tmp_out" >&2
fi
rm -f "$tmp_out"
return $rc
}
# Extract the primary image ref from a Docker tarball's manifest.json.
_image_name_from_tar() {
local tar_file="$1"
if command -v python3 >/dev/null 2>&1; then
python3 - "$tar_file" <<'PYEOF' 2>/dev/null
import json, sys, tarfile
try:
with tarfile.open(sys.argv[1]) as t:
tags = json.load(t.extractfile("manifest.json"))[0].get("RepoTags") or []
if tags:
print(tags[0])
except Exception:
pass
PYEOF
fi
}
_preload_docker_images() { _preload_docker_images() {
local prole_data local prole_data
prole_data=$(_resolve_prole_data) prole_data=$(_resolve_prole_data)
@ -173,26 +247,125 @@ _preload_docker_images() {
echo "WARN: k3d not available; skipping docker-import pre-load." >&2 echo "WARN: k3d not available; skipping docker-import pre-load." >&2
return 0 return 0
fi fi
local cluster_name="${K3D_CLUSTER_NAME:-prole-dev-cluster}" local cluster_name="${K3D_CLUSTER_NAME:-knoe-dev-cluster}"
echo " Importing all images into k3d cluster '$cluster_name' (bulk) ..."
local -a tar_array=() # ── Step 1: build indexed arrays of tar paths + resolved image names ──
while IFS= read -r line; do local -a tar_paths=()
[[ -n "$line" ]] && tar_array+=("$line") local -a tar_images=()
local _tf
while IFS= read -r _tf; do
[[ -z "$_tf" ]] && continue
tar_paths+=("$_tf")
tar_images+=("$(_image_name_from_tar "$_tf")")
done <<< "$tar_files" done <<< "$tar_files"
if [[ ${#tar_array[@]} -gt 0 ]]; then local total_tars=${#tar_paths[@]}
if k3d image import "${tar_array[@]}" -c "$cluster_name" >/dev/null 2>&1; then
echo " [OK] Bulk import of ${#tar_array[@]} images successful." if [[ $total_tars -eq 0 ]]; then
else echo " [SKIP] No tar files to process."
echo " [WARN] Bulk import failed; falling back to sequential." >&2 return 0
for tar_file in "${tar_array[@]}"; do fi
echo " Importing $(basename "$tar_file") into k3d cluster '$cluster_name' ..."
if k3d image import "$tar_file" -c "$cluster_name" >/dev/null 2>&1; then # ── Step 2: query images already in the k3d cluster ───────────────────
echo " [OK] $(basename "$tar_file")" echo " Checking k3d cluster '$cluster_name' for existing images ..."
else local existing_images=""
echo " [WARN] Failed to import $(basename "$tar_file"); image will be pulled at deploy time." >&2 local node_name="k3d-${cluster_name}-server-0"
fi if docker inspect "$node_name" >/dev/null 2>&1; then
done existing_images=$(docker exec "$node_name" \
ctr --address /run/k3s/containerd/containerd.sock \
--namespace k8s.io images ls -q 2>/dev/null || true)
else
echo " [WARN] k3d node '$node_name' not reachable; will import all images."
fi
# ── Step 3: delta — split into already-present vs needs-import ─────────
local -a to_import_tars=()
local -a to_import_images=()
local -a already_present=()
local _i
for (( _i=0; _i<total_tars; _i++ )); do
local _tar="${tar_paths[$_i]}"
local _img="${tar_images[$_i]}"
local _found=0
if [[ -n "$_img" && -n "$existing_images" ]]; then
# Strip digest suffix before matching
local _img_ref="${_img%%@*}"
if echo "$existing_images" | grep -qF "$_img_ref"; then
_found=1
fi
fi fi
if [[ $_found -eq 1 ]]; then
already_present+=("$(basename "$_tar")${_img:+ → $_img}")
else
to_import_tars+=("$_tar")
to_import_images+=("${_img:-}")
fi
done
# ── Step 4: report delta to user ───────────────────────────────────────
if [[ ${#already_present[@]} -gt 0 ]]; then
echo " Already in cluster — skipping ${#already_present[@]} image(s):"
local _entry
for _entry in "${already_present[@]}"; do
echo "$_entry"
done
fi
local import_count=${#to_import_tars[@]}
if [[ $import_count -eq 0 ]]; then
echo " [OK] All images already present in k3d cluster '$cluster_name'; nothing to import."
return 0
fi
echo " Bulk uploading $import_count image(s) into k3d cluster '$cluster_name':"
for (( _i=0; _i<import_count; _i++ )); do
local _lbl="${to_import_images[$_i]:-$(basename "${to_import_tars[$_i]}")}"
echo "$(basename "${to_import_tars[$_i]}")${_lbl:+ ($_lbl)}"
done
# ── Step 5: bulk import with live progress footer ──────────────────────
local overall_start elapsed_bulk
overall_start=$(date +%s)
if _exec_with_progress_footer \
"k3d image import — bulk ($import_count images) → $cluster_name" \
k3d image import "${to_import_tars[@]}" -c "$cluster_name"; then
elapsed_bulk=$(( $(date +%s) - overall_start ))
local bm=$(( elapsed_bulk / 60 )) bs=$(( elapsed_bulk % 60 ))
echo " [OK] Bulk import of $import_count image(s) complete (${bm}m${bs}s)."
else
echo " [WARN] Bulk import failed; falling back to sequential import." >&2
local _idx
for (( _i=0; _i<import_count; _i++ )); do
_idx=$(( _i + 1 ))
local _tar="${to_import_tars[$_i]}"
local _img="${to_import_images[$_i]:-$(basename "$_tar")}"
# ETA based on average time per completed image
local _now _elapsed_so_far _eta_str=""
_now=$(date +%s)
_elapsed_so_far=$(( _now - overall_start ))
if [[ $_i -gt 0 ]]; then
local _avg=$(( _elapsed_so_far / _i ))
local _remaining=$(( _avg * (import_count - _i) ))
local _rm=$(( _remaining / 60 )) _rs=$(( _remaining % 60 ))
_eta_str=" | ETA ~${_rm}m${_rs}s"
fi
echo " [$_idx/$import_count] $(basename "$_tar")${_img:+ ($_img)}${_eta_str}"
local _item_start
_item_start=$(date +%s)
if _exec_with_progress_footer \
"k3d image import [$_idx/$import_count] $(basename "$_tar")" \
k3d image import "$_tar" -c "$cluster_name"; then
local _item_elapsed=$(( $(date +%s) - _item_start ))
echo " [OK] $(basename "$_tar") (${_item_elapsed}s)"
else
echo " [WARN] Failed to import $(basename "$_tar"); will pull at deploy time." >&2
fi
done
local _total_elapsed=$(( $(date +%s) - overall_start ))
local _te_m=$(( _total_elapsed / 60 )) _te_s=$(( _total_elapsed % 60 ))
echo " [DONE] Sequential import complete. Total: ${_te_m}m${_te_s}s"
fi fi
;; ;;
k3s) k3s)

View File

@ -16,7 +16,7 @@ source "$SCRIPT_DIR/prole_cfg.sh"
VERBOSE=false VERBOSE=false
ENVIRONMENT="prod" ENVIRONMENT="prod"
HOST="" HOST=""
CLUSTER_NAME_DEFAULT="prole-dev-cluster" CLUSTER_NAME_DEFAULT="knoe-dev-cluster"
CLUSTER_PORT_DEFAULT="6443" CLUSTER_PORT_DEFAULT="6443"
usage() { usage() {

View File

@ -355,7 +355,7 @@ build_prole_kdc_image() {
if [[ -z "$PROLE_KDC_REGISTRY_HOST" && "$(normalized_mode)" == "k3d" ]]; then if [[ -z "$PROLE_KDC_REGISTRY_HOST" && "$(normalized_mode)" == "k3d" ]]; then
if command -v k3d >/dev/null 2>&1; then if command -v k3d >/dev/null 2>&1; then
local cluster_name="${K3D_CLUSTER_NAME:-prole-dev-cluster}" local cluster_name="${K3D_CLUSTER_NAME:-knoe-dev-cluster}"
log "Importing authority image into k3d cluster: ${cluster_name}" log "Importing authority image into k3d cluster: ${cluster_name}"
k3d image import "$local_tag" -c "$cluster_name" >/dev/null 2>&1 || true k3d image import "$local_tag" -c "$cluster_name" >/dev/null 2>&1 || true
fi fi

View File

@ -255,7 +255,7 @@ apply_k8s() {
} }
configure_k3d_insecure_registry() { configure_k3d_insecure_registry() {
local cluster_name="${1:-prole-dev-cluster}" local cluster_name="${1:-knoe-dev-cluster}"
local reg_host="k3d-prole-registry" local reg_host="k3d-prole-registry"
local reg_port="5000" local reg_port="5000"
local registries_yaml local registries_yaml
@ -291,7 +291,7 @@ apply_registry() {
if ! k3d registry list prole-registry >/dev/null 2>&1; then if ! k3d registry list prole-registry >/dev/null 2>&1; then
k3d registry create prole-registry --port 5000 || true k3d registry create prole-registry --port 5000 || true
fi fi
local cluster_name="${K3D_CLUSTER_NAME:-prole-dev-cluster}" local cluster_name="${K3D_CLUSTER_NAME:-knoe-dev-cluster}"
if k3d cluster list "$cluster_name" >/dev/null 2>&1; then if k3d cluster list "$cluster_name" >/dev/null 2>&1; then
echo "Connecting prole-registry to $cluster_name ..." echo "Connecting prole-registry to $cluster_name ..."
# k3d v5+ uses 'k3d-NAME' as container name for registries # k3d v5+ uses 'k3d-NAME' as container name for registries

View File

@ -166,7 +166,7 @@ deploy_service_layer() {
ensure_namespace "$ns" ensure_namespace "$ns"
label_namespace "$ns" label_namespace "$ns"
local argocd_action opentofu_action garage_action kdc_action openbao_action certmgr_action kong_action local argocd_action opentofu_action garage_action kdc_action openbao_action kong_action
case "$action" in case "$action" in
start|initialize|update|reload) argocd_action="update" ;; start|initialize|update|reload) argocd_action="update" ;;
restart) argocd_action="restart" ;; restart) argocd_action="restart" ;;
@ -191,14 +191,6 @@ deploy_service_layer() {
*) openbao_action="update" ;; *) openbao_action="update" ;;
esac esac
case "$action" in
start|initialize|update|reload) certmgr_action="update" ;;
restart) certmgr_action="restart" ;;
stop) certmgr_action="stop" ;;
status) certmgr_action="status" ;;
*) certmgr_action="update" ;;
esac
case "$action" in case "$action" in
start|initialize|update|reload) garage_action="start" ;; start|initialize|update|reload) garage_action="start" ;;
restart) garage_action="restart" ;; restart) garage_action="restart" ;;
@ -230,8 +222,8 @@ deploy_service_layer() {
OPENBAO_NAMESPACE="$ns" SERVICE_NAMESPACE="$ns" \ OPENBAO_NAMESPACE="$ns" SERVICE_NAMESPACE="$ns" \
"$SCRIPT_DIR/init_openbao.sh" -n "$ns" "$openbao_action" || rc=$? "$SCRIPT_DIR/init_openbao.sh" -n "$ns" "$openbao_action" || rc=$?
CERTMGR_NAMESPACE="$ns" SERVICE_NAMESPACE="$ns" \ # cert-manager is cluster-scoped and managed independently via init_certmgr.sh
"$SCRIPT_DIR/init_certmgr.sh" -n "$ns" "$certmgr_action" || rc=$? # in its own dedicated 'cert-manager' namespace; it is not part of the service layer.
NAMESPACE="$ns" SERVICE_NAMESPACE="$ns" GARAGE_NAMESPACE="$ns" \ NAMESPACE="$ns" SERVICE_NAMESPACE="$ns" GARAGE_NAMESPACE="$ns" \
"$SCRIPT_DIR/init_garage_store.sh" "$garage_action" || rc=$? "$SCRIPT_DIR/init_garage_store.sh" "$garage_action" || rc=$?

View File

@ -15,7 +15,7 @@ source "$SCRIPT_DIR/prole_cfg.sh"
log() { printf '%s\n' "$*"; } log() { printf '%s\n' "$*"; }
err() { printf '%s\n' "$*" >&2; } err() { printf '%s\n' "$*" >&2; }
cluster_name="${K3D_CLUSTER_NAME:-prole-dev-cluster}" cluster_name="${K3D_CLUSTER_NAME:-knoe-dev-cluster}"
ad_dc_ip="${AD_DC_IP:-${KDC_ANSIBLE_DETECTED:-${KDC_AUTO_DETECTED:-}}}" ad_dc_ip="${AD_DC_IP:-${KDC_ANSIBLE_DETECTED:-${KDC_AUTO_DETECTED:-}}}"
if [[ -z "$ad_dc_ip" ]]; then if [[ -z "$ad_dc_ip" ]]; then

View File

@ -176,6 +176,7 @@ prole_normalize_mode() {
service|k3s|k3s-*) echo "k3s"; return 0 ;; service|k3s|k3s-*) echo "k3s"; return 0 ;;
prod|production|k8s|k8s-*) echo "k8s"; return 0 ;; prod|production|k8s|k8s-*) echo "k8s"; return 0 ;;
prole-dev-cluster|k3d-prole-dev-cluster) echo "k3d"; return 0 ;; prole-dev-cluster|k3d-prole-dev-cluster) echo "k3d"; return 0 ;;
knoe-dev-cluster|k3d-knoe-dev-cluster) echo "k3d"; return 0 ;;
prole-service-cluster) echo "k3s"; return 0 ;; prole-service-cluster) echo "k3s"; return 0 ;;
prole-prod-cluster) echo "k8s"; return 0 ;; prole-prod-cluster) echo "k8s"; return 0 ;;
esac esac

View File

@ -143,7 +143,7 @@ cluster_ready() {
} }
k3d_cluster_ready() { k3d_cluster_ready() {
local cluster="${K3D_CLUSTER:-prole-dev-cluster}" local cluster="${K3D_CLUSTER:-knoe-dev-cluster}"
if have k3d; then if have k3d; then
if k3d cluster list --no-headers 2>/dev/null | grep -q "^${cluster}[[:space:]]"; then if k3d cluster list --no-headers 2>/dev/null | grep -q "^${cluster}[[:space:]]"; then
return 0 return 0

View File

@ -80,14 +80,14 @@ fi
ARGOCD_NS="${ARGOCD_NAMESPACE:-argocd}" ARGOCD_NS="${ARGOCD_NAMESPACE:-argocd}"
KONG_NS="${KONG_NAMESPACE:-${SERVICE_NAMESPACE:-${NAMESPACE:-}}}" KONG_NS="${KONG_NAMESPACE:-${SERVICE_NAMESPACE:-${NAMESPACE:-}}}"
CERTMGR_NS="${CERTMGR_NAMESPACE:-${SERVICE_NAMESPACE:-${NAMESPACE:-}}}" CERTMGR_NS="${CERTMGR_NAMESPACE:-cert-manager}"
KONG_NAME="${KONG_NAME:-prole-svc-kong}" KONG_NAME="${KONG_NAME:-prole-svc-kong}"
if [ -z "$KONG_NS" ]; then if [ -z "$KONG_NS" ]; then
KONG_NS="default" KONG_NS="default"
fi fi
if [ -z "$CERTMGR_NS" ]; then if [ -z "$CERTMGR_NS" ]; then
CERTMGR_NS="default" CERTMGR_NS="cert-manager"
fi fi
if ! command -v kubectl >/dev/null 2>&1; then if ! command -v kubectl >/dev/null 2>&1; then
@ -275,64 +275,257 @@ fi
if [ $missing -gt 0 ]; then if [ $missing -gt 0 ]; then
echo "" echo ""
echo "Status: FAILED ($missing resources missing)" # ── Attempt to repair a completely absent cert-manager before failing ──────
exit 1 if ! kubectl get namespace "$CERTMGR_NS" >/dev/null 2>&1 \
&& [[ -x "$SCRIPT_DIR/init_certmgr.sh" ]]; then
echo "== Repair: cert-manager namespace '$CERTMGR_NS' absent — re-initializing =="
"$SCRIPT_DIR/init_certmgr.sh" initialize 2>&1 || true
echo " Waiting for cert-manager to settle (20s)..."
sleep 20
echo " Re-checking cert-manager resources..."
_check_cm_resource() {
local _kind=$1 _name=$2 _ns=$3
if ! kubectl -n "$_ns" get "$_kind" "$_name" >/dev/null 2>&1; then
echo " [FAIL] $_kind/$_name still missing (ns=$_ns)"
else
missing=$((missing - 1))
echo " [OK] $_kind/$_name present (ns=$_ns)"
fi
}
_check_cm_resource svc cert-manager "$CERTMGR_NS"
_check_cm_resource svc cert-manager-webhook "$CERTMGR_NS"
_check_cm_resource deploy cert-manager "$CERTMGR_NS"
_check_cm_resource deploy cert-manager-cainjector "$CERTMGR_NS"
_check_cm_resource deploy cert-manager-webhook "$CERTMGR_NS"
fi
if [ $missing -gt 0 ]; then
echo "Status: FAILED ($missing resources missing)"
exit 1
fi
echo "Status: OK (missing resources restored by repair)"
fi fi
# ── Repair helpers ────────────────────────────────────────────────────────────
# Emit "kind name ns" lines for every workload owned by a component.
_workloads_for_comp() {
local _comp="$1"
local _ns="${COMP_NAMESPACE[$_comp]:-$NS}"
case "$_comp" in
argocd)
echo "statefulset argocd-application-controller $ARGOCD_NS"
for _d in argocd-server argocd-repo-server argocd-dex-server \
argocd-applicationset-controller argocd-notifications-controller argocd-redis; do
echo "deployment $_d $ARGOCD_NS"
done
;;
openbao)
if kubectl -n "$_ns" get statefulset openbao >/dev/null 2>&1; then
echo "statefulset openbao $_ns"
else
echo "deployment openbao $_ns"
fi
;;
opentofu) echo "deployment opentofu $_ns" ;;
garage) echo "statefulset garage $_ns" ;;
auth) echo "deployment auth $_ns" ;;
kong) echo "deployment $KONG_NAME $KONG_NS" ;;
certmgr)
for _d in cert-manager cert-manager-cainjector cert-manager-webhook; do
echo "deployment $_d $CERTMGR_NS"
done
;;
esac
}
# Reset all pod-tracking state and re-run analysis.
_recheck_pods() {
HEALTHY_COUNT=()
BLOCKED_COUNT=()
BLOCKED_PODS=()
COMP_NAMESPACE=()
blocked_lines=""
blocked=0
analyze_pods "$ARGOCD_NS" "argocd"
analyze_pods "$NS" "$pod_filter"
analyze_pods "$KONG_NS" "kong"
analyze_pods "$CERTMGR_NS" "cert-manager"
if [[ -n "$blocked_lines" ]]; then
blocked=1
fi
}
# Print the name of each component that currently has blocked pods.
_blocked_comps() {
for _bc in argocd openbao opentofu garage auth kong certmgr; do
[[ "$_bc" == "auth" && "$ENABLE_KERBEROS" != "1" ]] && continue
[[ ${BLOCKED_COUNT["$_bc"]:-0} -gt 0 ]] && echo "$_bc"
done
}
# ── Escalating repair ─────────────────────────────────────────────────────────
if [ $blocked -gt 0 ]; then if [ $blocked -gt 0 ]; then
repairable=()
fail_all=() # ── Round 1: delete blocked pods ──────────────────────────────────────────
for comp in argocd openbao opentofu garage auth kong certmgr; do echo ""
if [[ "$comp" == "auth" && "$ENABLE_KERBEROS" != "1" ]]; then echo "== Repair Round 1: replacing blocked pods =="
continue mapfile -t _r1_comps < <(_blocked_comps)
for _comp in "${_r1_comps[@]}"; do
_pods="${BLOCKED_PODS[$_comp]:-}"
_ns="${COMP_NAMESPACE[$_comp]:-$NS}"
if [[ -n "$_pods" ]]; then
echo " Deleting pod(s) for $_comp in $_ns:$_pods"
# shellcheck disable=SC2086
kubectl -n "$_ns" delete pod $_pods --wait=false >/dev/null 2>&1 || true
fi fi
hc=${HEALTHY_COUNT["$comp"]:-0} done
bc=${BLOCKED_COUNT["$comp"]:-0} echo " Waiting for pod replacement (15s)..."
if [[ "$bc" -gt 0 && "$hc" -gt 0 ]]; then sleep 15
repairable+=("$comp") _recheck_pods
elif [[ "$bc" -gt 0 && "$hc" -eq 0 ]]; then
fail_all+=("$comp") if [[ $blocked -eq 0 ]]; then
echo ""
echo "Status: OK (resolved in repair round 1)"
exit 0
fi
echo ""
echo "Pods still blocked after Round 1:"
printf '%b' "$blocked_lines"
# ── Round 2: rollout restart owning StatefulSet / Deployment ──────────────
echo ""
echo "== Repair Round 2: restarting workload controllers =="
mapfile -t _r2_comps < <(_blocked_comps)
for _comp in "${_r2_comps[@]}"; do
while IFS=' ' read -r _kind _name _wns; do
[[ -z "$_kind" ]] && continue
if kubectl -n "$_wns" get "$_kind" "$_name" >/dev/null 2>&1; then
echo " kubectl rollout restart $_kind/$_name -n $_wns"
kubectl -n "$_wns" rollout restart "$_kind/$_name" >/dev/null 2>&1 || true
kubectl -n "$_wns" rollout status "$_kind/$_name" --timeout=60s 2>/dev/null || true
fi
done < <(_workloads_for_comp "$_comp")
done
echo " Waiting for rollout to settle (20s)..."
sleep 20
_recheck_pods
if [[ $blocked -eq 0 ]]; then
echo ""
echo "Status: OK (resolved in repair round 2)"
exit 0
fi
echo ""
echo "Pods still blocked after Round 2:"
printf '%b' "$blocked_lines"
# ── Round 3: clear affected namespaces and re-initialize ──────────────────
echo ""
echo "== Repair Round 3: clearing affected namespaces and re-initializing =="
mapfile -t _r3_comps < <(_blocked_comps)
declare -A _cleared_ns=()
for _comp in "${_r3_comps[@]}"; do
_ns="${COMP_NAMESPACE[$_comp]:-$NS}"
[[ -n "${_cleared_ns[$_ns]:-}" ]] && continue
_cleared_ns["$_ns"]=1
if [[ "$_ns" == "default" || "$_ns" == "kube-system" ]]; then
echo " [Round 3] Removing workload resources in protected namespace '$_ns' for: $_comp"
while IFS=' ' read -r _kind _name _wns; do
[[ -z "$_kind" ]] && continue
echo " kubectl delete $_kind $_name -n $_wns --ignore-not-found"
kubectl -n "$_wns" delete "$_kind" "$_name" --ignore-not-found >/dev/null 2>&1 || true
done < <(_workloads_for_comp "$_comp")
else
echo " [Round 3] Deleting namespace '$_ns' ..."
kubectl delete namespace "$_ns" --wait=true --timeout=90s >/dev/null 2>&1 || true
echo " [Round 3] Recreating namespace '$_ns' ..."
kubectl create namespace "$_ns" >/dev/null 2>&1 || true
echo " [OK] Namespace '$_ns' cleared and recreated."
fi fi
done done
if [ ${#fail_all[@]} -gt 0 ]; then if [[ -x "$SCRIPT_DIR/init_service_layer.sh" ]]; then
echo "" echo " [Round 3] Re-initializing service layer ..."
echo "Repair skipped: all pods blocked for ${fail_all[*]}." _krb_flag=""
echo "Action: Deploy Missing Services." [[ "$ENABLE_KERBEROS" == "1" ]] && _krb_flag="-k"
echo "" # shellcheck disable=SC2086
echo "Status: NOTICE (blocked pods detected)" "$SCRIPT_DIR/init_service_layer.sh" -n "$NS" $_krb_flag update 2>&1 || true
exit 2
fi fi
if [ ${#repairable[@]} -gt 0 ]; then echo " [Round 3] Re-checking status after namespace reset (30s)..."
sleep 30
_recheck_pods
if [[ $blocked -eq 0 ]]; then
echo "" echo ""
echo "== Repair ==" echo "Status: OK (resolved in repair round 3)"
echo "Repairing blocked pods for: ${repairable[*]}" exit 0
for comp in "${repairable[@]}"; do
pods="${BLOCKED_PODS["$comp"]:-}"
ns="${COMP_NAMESPACE["$comp"]:-$NS}"
if [[ -n "$pods" ]]; then
kubectl -n "$ns" delete pod $pods --wait=false >/dev/null 2>&1 || true
fi
done
echo "Re-checking pods after repair..."
sleep 2
HEALTHY_COUNT=()
BLOCKED_COUNT=()
BLOCKED_PODS=()
COMP_NAMESPACE=()
blocked_lines=""
analyze_pods "$ARGOCD_NS" "argocd"
analyze_pods "$NS" "$pod_filter"
if [[ -n "$blocked_lines" ]]; then
echo ""
echo "Blocked pods still detected after repair:"
printf '%b' "$blocked_lines"
echo ""
echo "Status: NOTICE (blocked pods detected)"
exit 2
fi
fi fi
echo ""
echo "Pods still blocked after Round 3:"
printf '%b' "$blocked_lines"
# ── Round 4: reset k3d cluster, preserve registry ─────────────────────────
_prole_mode="${PROLE_MODE:-}"
if [[ "$_prole_mode" == "k3d" ]]; then
echo ""
echo "== Repair Round 4: resetting k3d cluster (preserving registry) =="
_cluster_name="${K3D_CLUSTER_NAME:-knoe-dev-cluster}"
_registry_args=()
if command -v k3d >/dev/null 2>&1; then
_reg_name=$(k3d registry list --no-headers 2>/dev/null | awk '{print $1}' | head -1 || true)
if [[ -n "$_reg_name" ]]; then
echo " Preserving registry: $_reg_name"
_registry_args=(--registry-use "$_reg_name")
fi
fi
echo " Deleting k3d cluster '$_cluster_name' ..."
k3d cluster delete "$_cluster_name" >/dev/null 2>&1 || true
echo " Recreating k3d cluster '$_cluster_name' ..."
k3d cluster create "$_cluster_name" -a 2 \
"${_registry_args[@]}" \
--api-port 0.0.0.0:6443 >/dev/null 2>&1 || true
kubectl config use-context "k3d-${_cluster_name}" >/dev/null 2>&1 || true
if [[ -x "$SCRIPT_DIR/init_service_layer.sh" ]]; then
echo " [Round 4] Re-initializing service layer after cluster reset ..."
_krb_flag=""
[[ "$ENABLE_KERBEROS" == "1" ]] && _krb_flag="-k"
# shellcheck disable=SC2086
"$SCRIPT_DIR/init_service_layer.sh" -n "$NS" $_krb_flag update 2>&1 || true
fi
echo " [Round 4] Re-checking status after cluster reset (30s)..."
sleep 30
_recheck_pods
if [[ $blocked -eq 0 ]]; then
echo ""
echo "Status: OK (resolved in repair round 4 — cluster reset)"
exit 0
fi
echo ""
echo "Pods still blocked after Round 4 (cluster reset):"
printf '%b' "$blocked_lines"
echo ""
echo "Status: FAILED — unable to repair after full cluster reset. Manual intervention required."
exit 5
fi
echo ""
echo "Status: NOTICE (blocked pods remain; k3d cluster reset not applicable for mode '${_prole_mode:-unknown}')"
exit 3
fi fi
echo "" echo ""

View File

@ -949,13 +949,13 @@ class ProleInstallerBase:
elif mode == "k3d": elif mode == "k3d":
# Ensure k3d dev clusters have a resolvable KUBECONFIG. # Ensure k3d dev clusters have a resolvable KUBECONFIG.
cluster_name = self._get_input("init_cluster.deployment_target", "prole-dev-cluster") cluster_name = self._get_input("init_cluster.deployment_target", "knoe-dev-cluster")
if not cluster_name or cluster_name == "prole-dev-cluster": if not cluster_name or cluster_name == "knoe-dev-cluster":
# Fallback to check if we have a more specific one from UI if available # Fallback to check if we have a more specific one from UI if available
cluster_name = getattr(self, "selected_k3d_cluster", None) cluster_name = getattr(self, "selected_k3d_cluster", None)
if hasattr(cluster_name, "get"): if hasattr(cluster_name, "get"):
cluster_name = cluster_name.get() cluster_name = cluster_name.get()
cluster_name = cluster_name or "prole-dev-cluster" cluster_name = cluster_name or "knoe-dev-cluster"
try: try:
subprocess.run( subprocess.run(
@ -2501,7 +2501,12 @@ class ProleSilentInstaller(ProleInstallerBase):
"PROLE_OPENTOFU_URL": _default_opentofu_pipeline_url(), "PROLE_OPENTOFU_URL": _default_opentofu_pipeline_url(),
"SERVICE_NAMESPACE": self._service_namespace(), "SERVICE_NAMESPACE": self._service_namespace(),
} }
globals_to_save.update(self.prole_cfg_data.get("Global", {})) # Merge pre-existing Global values without overriding explicit inputs
existing_global = dict(self.prole_cfg_data.get("Global", {}))
for k, v in existing_global.items():
if k not in globals_to_save or not str(globals_to_save.get(k, "")).strip():
globals_to_save[k] = v
# Re-assert critical/dynamic values to avoid accidental overwrite
globals_to_save["DB_PASSWORD"] = self._secret_cfg_value( globals_to_save["DB_PASSWORD"] = self._secret_cfg_value(
"Global", "DB_PASSWORD", db_pw, "db", "password" "Global", "DB_PASSWORD", db_pw, "db", "password"
) )
@ -2591,8 +2596,8 @@ class ProleSilentInstaller(ProleInstallerBase):
sections["Dev Cluster (k3d)"] = { sections["Dev Cluster (k3d)"] = {
**self.prole_cfg_data.get("Dev Cluster (k3d)", {}), **self.prole_cfg_data.get("Dev Cluster (k3d)", {}),
"MODE": "k3d", "MODE": "k3d",
"CLUSTER_ENV": "k3d-prole-dev-cluster", "CLUSTER_ENV": "k3d-knoe-dev-cluster",
"DISPLAY_NAME": "prole-dev-cluster", "DISPLAY_NAME": "knoe-dev-cluster",
"KUBECTL_CONTEXT": self._get_input("init_cluster.cluster_env", ""), "KUBECTL_CONTEXT": self._get_input("init_cluster.cluster_env", ""),
} }
sections["Service Cluster (k3s)"] = { sections["Service Cluster (k3s)"] = {
@ -2993,7 +2998,7 @@ class ProleSilentInstaller(ProleInstallerBase):
env_key = _normalize_cluster_env( env_key = _normalize_cluster_env(
self._get_input("init_cluster.cluster_env", "dev") self._get_input("init_cluster.cluster_env", "dev")
) )
cluster_name = "prole-dev-cluster" cluster_name = "knoe-dev-cluster"
# Check if image already exists # Check if image already exists
rc_inspect = subprocess.run( rc_inspect = subprocess.run(
@ -3047,7 +3052,7 @@ class ProleSilentInstaller(ProleInstallerBase):
self.log("[OK] Build successful.") self.log("[OK] Build successful.")
push_ok = True push_ok = True
if env_key == "dev": if env_key == "dev":
cluster_name = "prole-dev-cluster" cluster_name = "knoe-dev-cluster"
self.log(f"Importing image to {cluster_name}...") self.log(f"Importing image to {cluster_name}...")
self._run_cmd( self._run_cmd(
["k3d", "image", "import", image_name, "-c", cluster_name] ["k3d", "image", "import", image_name, "-c", cluster_name]
@ -3141,7 +3146,7 @@ class ProleSilentInstaller(ProleInstallerBase):
if not self.controller.check_docker_running(): if not self.controller.check_docker_running():
raise Exception("Docker is not running.") raise Exception("Docker is not running.")
cluster_name = "prole-dev-cluster" cluster_name = "knoe-dev-cluster"
res = subprocess.run( res = subprocess.run(
["k3d", "cluster", "list", "--no-headers"], ["k3d", "cluster", "list", "--no-headers"],
capture_output=True, capture_output=True,
@ -3542,7 +3547,6 @@ class ProleSilentInstaller(ProleInstallerBase):
[ [
("init_prole-db-backup.sh", ["start"], False), ("init_prole-db-backup.sh", ["start"], False),
("init_kong.sh", ["start"], False), ("init_kong.sh", ["start"], False),
("init_db_manager.sh", ["start"], False),
("init_monitoring.sh", ["initialize"], False), ("init_monitoring.sh", ["initialize"], False),
] ]
) )
@ -3821,7 +3825,7 @@ class ProleSilentInstaller(ProleInstallerBase):
if not self.controller.check_docker_running(): if not self.controller.check_docker_running():
raise Exception("Docker is not running.") raise Exception("Docker is not running.")
cluster_name = "prole-dev-cluster" cluster_name = "knoe-dev-cluster"
rc = self._run_cmd(["k3d", "cluster", "delete", cluster_name]) rc = self._run_cmd(["k3d", "cluster", "delete", cluster_name])
if rc != 0: if rc != 0:
self.err( self.err(

View File

@ -844,7 +844,7 @@ def _normalize_cluster_env(env: str | None) -> str:
return "" return ""
s = str(env).strip().lower() s = str(env).strip().lower()
if ( if (
s in ("dev", "k3d", "k3d-dev", "k3d-prole-dev-cluster", "prole-dev-cluster") s in ("dev", "k3d", "k3d-dev", "k3d-knoe-dev-cluster", "knoe-dev-cluster")
or s.startswith("k3d-") or s.startswith("k3d-")
or s.startswith("prole-dev-") or s.startswith("prole-dev-")
): ):
@ -874,7 +874,7 @@ def _cluster_env_radio_value(env: str | None) -> str:
def _deployment_target_label(env: str | None) -> str: def _deployment_target_label(env: str | None) -> str:
key = _normalize_cluster_env(env) key = _normalize_cluster_env(env)
if key == "dev": if key == "dev":
return "prole-dev-cluster" return "knoe-dev-cluster"
if key == "service": if key == "service":
return "prole-service-cluster" return "prole-service-cluster"
if key == "prod": if key == "prod":
@ -1610,8 +1610,9 @@ def _render_prole_cfg(
def first_non_empty(*vals: str) -> str: def first_non_empty(*vals: str) -> str:
for val in vals: for val in vals:
if val: v = str(val or "").strip()
return val if v and not (v.startswith("${") and v.endswith("}")):
return v
return "" return ""
def get_input(key: str) -> str: def get_input(key: str) -> str:
@ -2066,16 +2067,6 @@ def _build_required_port_forwards(
"TCP", "TCP",
"Grafana UI", "Grafana UI",
), ),
_pf_mapping_str(
"prole-db-manager",
db_ns,
"svc/prole-db-manager",
"80",
"80",
addr_all,
"TCP",
"DB Manager UI",
),
] ]
if supabase_enabled: if supabase_enabled:

View File

@ -438,7 +438,7 @@ class ClusterLifecycleMilestone(Milestone):
self.logger.error("Docker is not running.") self.logger.error("Docker is not running.")
return return
cluster_name = "prole-dev-cluster" cluster_name = "knoe-dev-cluster"
res = subprocess.run( res = subprocess.run(
["k3d", "cluster", "list", "--no-headers"], ["k3d", "cluster", "list", "--no-headers"],
capture_output=True, capture_output=True,
@ -665,7 +665,6 @@ class InitializationScriptsMilestone(Milestone):
[ [
"init_prole-db-backup.sh", "init_prole-db-backup.sh",
"init_kong.sh", "init_kong.sh",
"init_db_manager.sh",
"init_monitoring.sh", "init_monitoring.sh",
] ]
) )
@ -692,7 +691,7 @@ class InitializationScriptsMilestone(Milestone):
args = mode_args + ["initialize"] args = mode_args + ["initialize"]
elif script == "init_prole-db-backup.sh": elif script == "init_prole-db-backup.sh":
args = mode_args + ["start"] args = mode_args + ["start"]
elif script in ("init_kong.sh", "init_db_manager.sh"): elif script == "init_kong.sh":
args = mode_args + ["start"] args = mode_args + ["start"]
elif script in ("init_monitoring.sh", "init_nginx_ingress.sh"): elif script in ("init_monitoring.sh", "init_nginx_ingress.sh"):
args = mode_args + ["initialize"] args = mode_args + ["initialize"]

View File

@ -128,7 +128,7 @@ class Milestone(ABC):
if not (env.get("KUBECONFIG") or "").strip(): if not (env.get("KUBECONFIG") or "").strip():
import subprocess as _sp import subprocess as _sp
cluster_name = "prole-dev-cluster" cluster_name = "knoe-dev-cluster"
try: try:
_sp.run( _sp.run(
[ [

View File

@ -52,9 +52,9 @@ class ProleNcursesInstaller:
self.selected_local_path = None self.selected_local_path = None
# Environment/config variables # Environment/config variables
self.cluster_env = "k3d-prole-dev-cluster" self.cluster_env = "k3d-knoe-dev-cluster"
self.cluster_environments = [ self.cluster_environments = [
"prole-dev-cluster", "knoe-dev-cluster",
"prole-service-cluster", "prole-service-cluster",
"prole-prod-cluster", "prole-prod-cluster",
] ]
@ -386,7 +386,7 @@ class ProleNcursesInstaller:
return True return True
selected_env = self.cluster_environments[self.selected_env_index] selected_env = self.cluster_environments[self.selected_env_index]
if selected_env == "prole-dev-cluster": if selected_env == "knoe-dev-cluster":
if key == ord(" "): # Toggle/Select context if key == ord(" "): # Toggle/Select context
pass # We could implement sub-selection pass # We could implement sub-selection
elif key == curses.KEY_NPAGE: # Page Down to scroll context list? elif key == curses.KEY_NPAGE: # Page Down to scroll context list?
@ -851,7 +851,7 @@ class ProleNcursesInstaller:
y += 1 y += 1
selected_env = self.cluster_environments[self.selected_env_index] selected_env = self.cluster_environments[self.selected_env_index]
if selected_env == "prole-dev-cluster": if selected_env == "knoe-dev-cluster":
win.render_text( win.render_text(
y, y,
4, 4,

View File

@ -346,8 +346,8 @@ class ProleInstaller(
("System Environment", "env_setup"), ("System Environment", "env_setup"),
("Cluster Environment", "init_cluster"), ("Cluster Environment", "init_cluster"),
("Database Options", "database_options"), ("Database Options", "database_options"),
("Database Creation", "init_password"),
("Docker Build", "init_db_build"), ("Docker Build", "init_db_build"),
("Database Creation", "init_password"),
("Initialization Scripts", "init_scripts"), ("Initialization Scripts", "init_scripts"),
("Kerberos Authentication", "kerberos_config"), ("Kerberos Authentication", "kerberos_config"),
("GitOps", "gitops_config"), ("GitOps", "gitops_config"),
@ -533,7 +533,7 @@ class ProleInstaller(
# Cluster / k3d variables # Cluster / k3d variables
self.cluster_env = tk.StringVar(value="dev") self.cluster_env = tk.StringVar(value="dev")
self.k3d_cluster_name = tk.StringVar(value="prole-dev-cluster") self.k3d_cluster_name = tk.StringVar(value="knoe-dev-cluster")
self.k3d_cluster_list = tk.Variable(value=[]) self.k3d_cluster_list = tk.Variable(value=[])
self.selected_k3d_cluster = tk.StringVar() self.selected_k3d_cluster = tk.StringVar()
try: try:

View File

@ -82,8 +82,12 @@ class ConfigMixin:
"DB_HOST_PORT": (self.db_host_port.get() or "5432").strip(), "DB_HOST_PORT": (self.db_host_port.get() or "5432").strip(),
"PROLE_OPENTOFU_URL": _default_opentofu_pipeline_url(), "PROLE_OPENTOFU_URL": _default_opentofu_pipeline_url(),
} }
# Add any other values already in self.prole_cfg_data['Global'] # Merge any other values already present in existing Global without overriding explicit UI values
globals_to_save.update(self.prole_cfg_data.get("Global", {})) existing_global = dict(self.prole_cfg_data.get("Global", {}))
for k, v in existing_global.items():
if k not in globals_to_save or not str(globals_to_save.get(k, "")).strip():
globals_to_save[k] = v
# Re-assert dynamic/derived globals to ensure they are not overwritten by existing values
globals_to_save["DB_PASSWORD"] = self._secret_cfg_value( globals_to_save["DB_PASSWORD"] = self._secret_cfg_value(
"Global", "DB_PASSWORD", self.db_password.get(), "db", "password" "Global", "DB_PASSWORD", self.db_password.get(), "db", "password"
) )
@ -135,7 +139,7 @@ class ConfigMixin:
**self.prole_cfg_data.get("Dev Cluster (k3d)", {}), **self.prole_cfg_data.get("Dev Cluster (k3d)", {}),
"MODE": "k3d", "MODE": "k3d",
"CLUSTER_ENV": "dev", "CLUSTER_ENV": "dev",
"DISPLAY_NAME": "prole-dev-cluster", "DISPLAY_NAME": "knoe-dev-cluster",
"KUBECTL_CONTEXT": (self.selected_kubectx.get() or "").strip(), "KUBECTL_CONTEXT": (self.selected_kubectx.get() or "").strip(),
} }
sections["Service Cluster (k3s)"] = { sections["Service Cluster (k3s)"] = {
@ -270,6 +274,15 @@ class ConfigMixin:
), ),
) )
# Database options (distribution, version, extensions)
_set("database_options.distribution", _get_var(self.db_distribution, "percona"))
_set("database_options.version_type", _get_var(self.db_version_type, "v18"))
for _ext in getattr(self, "extensions_list", []):
_set_bool(
f"database_options.ext.{_ext['id']}",
_get_var(self.db_extensions.get(_ext["id"]), False),
)
# Build DB image # Build DB image
_set_bool( _set_bool(
"init_db_build.run_build", "init_db_build.run_build",

View File

@ -1182,7 +1182,7 @@ class ClusterScreenMixin:
"""Verify the config then write the values to prole.cfg.""" """Verify the config then write the values to prole.cfg."""
env_key = self._cluster_env_key() env_key = self._cluster_env_key()
if env_key == "dev": if env_key == "dev":
cluster_val = self.selected_k3d_cluster.get() or "prole-dev-cluster" cluster_val = self.selected_k3d_cluster.get() or "knoe-dev-cluster"
if not cluster_val: if not cluster_val:
messagebox.showwarning( messagebox.showwarning(
"Validation", "Please select or create a k3d cluster." "Validation", "Please select or create a k3d cluster."
@ -1266,7 +1266,7 @@ class ClusterScreenMixin:
cluster_ok = False cluster_ok = False
if env == "dev": if env == "dev":
cluster_name = self.selected_k3d_cluster.get() or "prole-dev-cluster" cluster_name = self.selected_k3d_cluster.get() or "knoe-dev-cluster"
try: try:
res = subprocess.run( res = subprocess.run(
["k3d", "cluster", "list", "--no-headers"], ["k3d", "cluster", "list", "--no-headers"],
@ -1457,7 +1457,7 @@ class ClusterScreenMixin:
return return
# 2. Manage or verify cluster # 2. Manage or verify cluster
cluster_name = "prole-dev-cluster" cluster_name = "knoe-dev-cluster"
res = subprocess.run( res = subprocess.run(
["k3d", "cluster", "list", "--no-headers"], ["k3d", "cluster", "list", "--no-headers"],
capture_output=True, capture_output=True,
@ -1612,20 +1612,6 @@ class ClusterScreenMixin:
on_line=_on_line, on_line=_on_line,
) )
# Also run status to show per-service output as requested
console.write("\n" + "="*60 + "\n")
console.write("Detailed Service Status:\n")
console.write("="*60 + "\n")
status_args = ["-n", ns, "status"]
self.controller.run_script(
"init_common_services.sh",
args=status_args,
env=env,
on_line=lambda line: console.write(line),
)
console.write(f"\nVerification Exit status: {rc}\n") console.write(f"\nVerification Exit status: {rc}\n")
# Parse per-component traffic light status from [OK]/[FAIL] lines # Parse per-component traffic light status from [OK]/[FAIL] lines

View File

@ -1282,7 +1282,7 @@ class DatabaseScreenMixin:
) )
# Import to k3d # Import to k3d
cluster_name = "prole-dev-cluster" cluster_name = "knoe-dev-cluster"
self._db_build_console.write( self._db_build_console.write(
f"Importing image to {cluster_name} ({import_tag})...\n" f"Importing image to {cluster_name} ({import_tag})...\n"
) )

View File

@ -14,14 +14,23 @@ class DatabaseOptionsScreenMixin:
"""Mixin for the Database Options screen.""" """Mixin for the Database Options screen."""
def _init_database_options_state(self): def _init_database_options_state(self):
# Load saved inputs (available when prole.cfg was previously saved)
_saved = getattr(self, "prole_cfg_data", {}).get("Inputs", {})
self.db_at_rest_encryption = tk.BooleanVar(value=True) self.db_at_rest_encryption = tk.BooleanVar(value=True)
self.db_distribution = tk.StringVar(value="percona") _saved_dist = _saved.get("database_options.distribution", "percona")
self.db_version_type = tk.StringVar(value="latest") # stable, current, latest, v18 self.db_distribution = tk.StringVar(value=_saved_dist or "percona")
self.db_selected_version = tk.StringVar(value="17") _saved_vtype = _saved.get("database_options.version_type", "v18")
self.db_version_type = tk.StringVar(value=_saved_vtype or "v18")
# Derive selected version from version_type; default to pg18
_vtype_to_ver = {"stable": "15", "current": "16", "latest": "17", "v18": "18"}
self.db_selected_version = tk.StringVar(
value=_vtype_to_ver.get(self.db_version_type.get(), "18")
)
# Extensions state: dict of id -> BooleanVar # Extensions state: dict of id -> BooleanVar
self.db_extensions = {} self.db_extensions = {}
self.extensions_list = [ self.extensions_list = [
{"id": "postgis", "name": "PostGIS", "description": "Spatial and geographic objects for PostgreSQL"}, {"id": "postgis", "name": "PostGIS", "description": "Spatial and geographic objects for PostgreSQL"},
{"id": "pgvector", "name": "pgvector", "description": "Vector similarity search"}, {"id": "pgvector", "name": "pgvector", "description": "Vector similarity search"},
@ -37,14 +46,18 @@ class DatabaseOptionsScreenMixin:
{"id": "pg_stat_monitor", "name": "pg_stat_monitor", "description": "Statistics collector (Percona only)", "percona_only": True}, {"id": "pg_stat_monitor", "name": "pg_stat_monitor", "description": "Statistics collector (Percona only)", "percona_only": True},
{"id": "pgbadger", "name": "pgBadger", "description": "Log analyzer (Package only, no extension)", "no_extension": True}, {"id": "pgbadger", "name": "pgBadger", "description": "Log analyzer (Package only, no extension)", "no_extension": True},
] ]
# Default enabled extensions # Default enabled extensions (all on for percona v18)
default_enabled = ["postgis", "pgvector", "pgcrypto", "pgaudit", "pg_repack", "pg_stat_statements", "pg_buffercache", "pg_freespacemap", "pgrowlocks", "postgres_fdw", "dblink"] default_enabled = ["postgis", "pgvector", "pgcrypto", "pgaudit", "pg_repack", "pg_stat_statements", "pg_buffercache", "pg_freespacemap", "pgrowlocks", "postgres_fdw", "dblink", "pg_stat_monitor", "pgbadger"]
for ext in self.extensions_list: for ext in self.extensions_list:
is_enabled = ext["id"] in default_enabled saved_key = f"database_options.ext.{ext['id']}"
if saved_key in _saved:
is_enabled = _saved[saved_key].lower() == "true"
else:
is_enabled = ext["id"] in default_enabled
self.db_extensions[ext["id"]] = tk.BooleanVar(value=is_enabled) self.db_extensions[ext["id"]] = tk.BooleanVar(value=is_enabled)
self.db_versions_data = {} self.db_versions_data = {}
self._load_database_versions() self._load_database_versions()

View File

@ -471,7 +471,7 @@ class DeployScreenMixin:
env_combo.pack(side="left") env_combo.pack(side="left")
env_help = tk.Label( env_help = tk.Label(
env_frame, env_frame,
text="Dev = local k3d (prole-dev-cluster), Service = remote k3s at pi.prole.org:6443, Prod = stretch of prole-service-cluster", text="Dev = local k3d (knoe-dev-cluster), Service = remote k3s at pi.prole.org:6443, Prod = stretch of prole-service-cluster",
bg="#1a1a1a", bg="#1a1a1a",
fg="#888888", fg="#888888",
font=("Helvetica", 9), font=("Helvetica", 9),
@ -776,7 +776,7 @@ kerberos.kdc={self.kerberos_kdc.get()}
self.update_deploy_step_status( self.update_deploy_step_status(
"Import image to k3d cluster (Dev only)", "running" "Import image to k3d cluster (Dev only)", "running"
) )
self.import_k3d_image(cluster_name="prole-dev-cluster") self.import_k3d_image(cluster_name="knoe-dev-cluster")
self.update_deploy_step_status( self.update_deploy_step_status(
"Import image to k3d cluster (Dev only)", "completed" "Import image to k3d cluster (Dev only)", "completed"
) )

View File

@ -500,7 +500,7 @@ class DockerScreenMixin:
if not _push_docker_image(remote_tag): if not _push_docker_image(remote_tag):
raise Exception("Failed to push image to registry.") raise Exception("Failed to push image to registry.")
def import_k3d_image(self, cluster_name="prole-dev-cluster"): def import_k3d_image(self, cluster_name="knoe-dev-cluster"):
"""Import image to k3d cluster (only for Dev).""" """Import image to k3d cluster (only for Dev)."""
version = self.get_prole_db_version() version = self.get_prole_db_version()
image_name = f"prole-db:{version}" image_name = f"prole-db:{version}"
@ -580,7 +580,7 @@ class DockerScreenMixin:
else: else:
raise Exception(f"Unknown environment: {env}") raise Exception(f"Unknown environment: {env}")
def create_or_recreate_k3d_dev_cluster(self, cluster_name="prole-dev-cluster"): def create_or_recreate_k3d_dev_cluster(self, cluster_name="knoe-dev-cluster"):
"""Create or restart local k3d cluster and wire it to the chosen registry.""" """Create or restart local k3d cluster and wire it to the chosen registry."""
result = subprocess.run( result = subprocess.run(
["k3d", "cluster", "list"], capture_output=True, text=True ["k3d", "cluster", "list"], capture_output=True, text=True

View File

@ -328,13 +328,13 @@ class NavigationMixin:
self.show_page("init_cluster") self.show_page("init_cluster")
return return
if current_id == "init_password": if current_id == "init_password":
self.show_page("database_options") self.show_page("init_db_build")
return return
if current_id == "init_db_build": if current_id == "init_db_build":
self.show_page("init_password") self.show_page("database_options")
return return
if current_id == "init_scripts": if current_id == "init_scripts":
self.show_page("init_db_build") self.show_page("init_password")
return return
if current_id == "kerberos_config": if current_id == "kerberos_config":
self.show_page("init_scripts") self.show_page("init_scripts")
@ -488,10 +488,7 @@ class NavigationMixin:
return return
if current_id == "init_db_build": if current_id == "init_db_build":
if self.kerberos_enabled.get(): self.show_page("init_password")
self.show_page("kerberos_config")
else:
self.show_page("init_scripts")
return return
if current_id == "build": if current_id == "build":
@ -519,7 +516,7 @@ class NavigationMixin:
if current_id == "database_options": if current_id == "database_options":
if self._generate_prole_db_dockerfile(): if self._generate_prole_db_dockerfile():
self.show_page("init_password") self.show_page("init_db_build")
return return
if current_id == "init_scripts": if current_id == "init_scripts":
@ -527,7 +524,10 @@ class NavigationMixin:
"Completed" if getattr(self, "_scripts_success", False) else "Attempted" "Completed" if getattr(self, "_scripts_success", False) else "Attempted"
) )
self._save_prole_cfg() self._save_prole_cfg()
self.show_page("kerberos_config") if self.kerberos_enabled.get():
self.show_page("kerberos_config")
else:
self.show_page("gitops_config")
return return
if current_id == "kerberos_config": if current_id == "kerberos_config":

View File

@ -160,7 +160,6 @@ class ServicesScreenMixin:
] ]
scripts.append(("Prole DB Backup", "init_prole-db-backup.sh")) scripts.append(("Prole DB Backup", "init_prole-db-backup.sh"))
scripts.append(("Kong API Gateway", "init_kong.sh")) scripts.append(("Kong API Gateway", "init_kong.sh"))
scripts.append(("DB Manager", "init_db_manager.sh"))
scripts.append(("Monitoring", "init_monitoring.sh")) scripts.append(("Monitoring", "init_monitoring.sh"))
mode = self._deployment_mode() mode = self._deployment_mode()
if mode != "k3d": if mode != "k3d":
@ -319,7 +318,7 @@ class ServicesScreenMixin:
"Skipping common services initialization because previous steps failed.\n" "Skipping common services initialization because previous steps failed.\n"
) )
# 2. init_cloudnative_pg.sh initialize # 3. init_cloudnative_pg.sh initialize
if overall_success: if overall_success:
script = "init_cloudnative_pg.sh" script = "init_cloudnative_pg.sh"
_select_tab(script) _select_tab(script)
@ -370,7 +369,7 @@ class ServicesScreenMixin:
"Skipping CloudNative-PG initialization because previous steps failed.\n" "Skipping CloudNative-PG initialization because previous steps failed.\n"
) )
# 3. init_prole-db-backup.sh start # 4. init_prole-db-backup.sh start
if overall_success: if overall_success:
script = "init_prole-db-backup.sh" script = "init_prole-db-backup.sh"
_select_tab(script) _select_tab(script)
@ -417,7 +416,7 @@ class ServicesScreenMixin:
"Skipping Prole DB backup because previous steps failed.\n" "Skipping Prole DB backup because previous steps failed.\n"
) )
# 4. init_kong.sh start # 5. init_kong.sh start
if overall_success: if overall_success:
script = "init_kong.sh" script = "init_kong.sh"
if script in self.script_consoles: if script in self.script_consoles:
@ -466,56 +465,7 @@ class ServicesScreenMixin:
"Skipping Kong because previous steps failed.\n" "Skipping Kong because previous steps failed.\n"
) )
# 5. init_db_manager.sh start # 7. init_monitoring.sh initialize
if overall_success:
script = "init_db_manager.sh"
if script in self.script_consoles:
_select_tab(script)
self.script_consoles[script].clear()
self.script_consoles[script].write(f"Running {script} start...\n")
log_path = _log_path_for(script)
self._record_install_log(log_path)
try:
log_fp = log_path.open("a", encoding="utf-8")
except Exception:
log_fp = None
def _dbmgr_line(line):
self.script_consoles[script].write(line)
self._process_script_output_line(line)
if log_fp:
try:
log_fp.write(line)
log_fp.flush()
except Exception:
pass
rc_dbmgr = self.controller.run_script(
script, args=mode_args + ["start"], env=env, on_line=_dbmgr_line
)
if log_fp:
try:
log_fp.close()
except Exception:
pass
if rc_dbmgr != 0:
self.script_consoles[script].write(
f"\nERROR: {script} start failed with code {rc_dbmgr}\n"
)
overall_success = False
else:
self.script_consoles[script].write(
f"\n{script} completed successfully.\n"
)
else:
if "init_db_manager.sh" in self.script_consoles:
self.script_consoles["init_db_manager.sh"].write(
"Skipping DB Manager because previous steps failed.\n"
)
# 6. init_monitoring.sh initialize
if overall_success: if overall_success:
script = "init_monitoring.sh" script = "init_monitoring.sh"
_select_tab(script) _select_tab(script)
@ -574,7 +524,7 @@ class ServicesScreenMixin:
"Skipping Monitoring initialization because previous steps failed.\n" "Skipping Monitoring initialization because previous steps failed.\n"
) )
# 7. init_nginx_ingress.sh initialize (k3s only — not used in k3d) # 8. init_nginx_ingress.sh initialize (k3s only — not used in k3d)
if overall_success and mode != "k3d": if overall_success and mode != "k3d":
script = "init_nginx_ingress.sh" script = "init_nginx_ingress.sh"
_select_tab(script) _select_tab(script)
@ -622,7 +572,7 @@ class ServicesScreenMixin:
"Skipping Nginx Ingress because previous steps failed.\n" "Skipping Nginx Ingress because previous steps failed.\n"
) )
# 8. init_port_forwards.sh start (k3d only — not used in k3s) # 9. init_port_forwards.sh start (k3d only — not used in k3s)
if overall_success and mode != "k3s": if overall_success and mode != "k3s":
script = "init_port_forwards.sh" script = "init_port_forwards.sh"
if script in self.script_consoles: if script in self.script_consoles:
@ -967,7 +917,7 @@ class ServicesScreenMixin:
) )
self._canvas_items.append(mode_label) self._canvas_items.append(mode_label)
mode_values = [ mode_values = [
"prole-dev-cluster", "knoe-dev-cluster",
"prole-service-cluster", "prole-service-cluster",
"prole-prod-cluster", "prole-prod-cluster",
] ]
@ -1784,6 +1734,6 @@ class ServicesScreenMixin:
else None else None
) )
) )
self.root.after(1000, lambda: self.show_page("init_db_build")) self.root.after(1000, lambda: self.show_page("init_scripts"))
threading.Thread(target=worker, daemon=True).start() threading.Thread(target=worker, daemon=True).start()

View File

@ -5,7 +5,7 @@ metadata:
name: prole-db name: prole-db
spec: spec:
instances: 3 instances: 3
imageName: k3d-prole-registry:5000/prole-db:18-070 imageName: k3d-prole-registry:5000/prole-db:18-089
postgresUID: 100 postgresUID: 100
postgresGID: 101 postgresGID: 101
maxSyncReplicas: 1 maxSyncReplicas: 1

View File

@ -4,7 +4,7 @@ metadata:
name: prole-db name: prole-db
spec: spec:
instances: 3 instances: 3
imageName: k3d-prole-registry:5000/prole-db:18-070 imageName: k3d-prole-registry:5000/prole-db:18-089
postgresUID: 100 postgresUID: 100
postgresGID: 101 postgresGID: 101
maxSyncReplicas: 1 maxSyncReplicas: 1

132
mock_val/common_core_lib.sh Normal file
View File

@ -0,0 +1,132 @@
#!/usr/bin/env bash
# Shared helpers for common core init scripts (ArgoCD, OpenBao, OpenTofu, Garage).
# Responsibilities:
# - handle [-c|--config] early so prole_cfg.sh loads the right prole.cfg
# - parse standard actions/options
# - resolve and apply namespaces consistently
# Actions supported by all common core scripts
COMMON_CORE_ACTIONS="start|stop|status|restart|initialize|update|reload"
# Parse -c/--config before loading prole_cfg.sh so PROLE_CONF is set in time.
# Sets:
# COMMON_CORE_CONFIG_PATH - path to prole.cfg (if provided)
# COMMON_CORE_ARGS - original arguments minus -c/--config
common_core_preparse_config() {
COMMON_CORE_CONFIG_PATH=""
COMMON_CORE_ARGS=()
while [[ $# -gt 0 ]]; do
case "$1" in
-c|--config)
shift
if [[ -z "${1:-}" ]]; then
echo "ERROR: -c/--config requires a file path" >&2
exit 2
fi
COMMON_CORE_CONFIG_PATH="$1"
;;
-c=*|--config=*)
COMMON_CORE_CONFIG_PATH="${1#*=}"
;;
*)
COMMON_CORE_ARGS+=("$1")
;;
esac
shift
done
if [[ -n "$COMMON_CORE_CONFIG_PATH" ]]; then
if [[ ! -f "$COMMON_CORE_CONFIG_PATH" ]]; then
echo "ERROR: config file not found: $COMMON_CORE_CONFIG_PATH" >&2
exit 2
fi
local cfg_dir
cfg_dir=$(cd "$(dirname "$COMMON_CORE_CONFIG_PATH")" && pwd)
PROLE_CONF="$cfg_dir"
export PROLE_CONF
fi
}
# Parse standard options and action.
# Sets (always):
# COMMON_CORE_ACTION
# COMMON_CORE_NAMESPACE
# COMMON_CORE_HELP (0/1)
# COMMON_CORE_PARSE_ERROR (empty or message)
common_core_parse_args() {
COMMON_CORE_ACTION=""
COMMON_CORE_NAMESPACE=""
COMMON_CORE_HELP=0
COMMON_CORE_PARSE_ERROR=""
while [[ $# -gt 0 ]]; do
case "$1" in
-m|--mode)
shift
prole_set_mode "${1:-}"
;;
-m=*|--mode=*)
prole_set_mode "${1#*=}"
;;
-n|--namespace)
shift
if [[ -z "${1:-}" ]]; then
COMMON_CORE_PARSE_ERROR="--namespace requires a value"
break
fi
COMMON_CORE_NAMESPACE="$1"
;;
-n=*|--namespace=*)
COMMON_CORE_NAMESPACE="${1#*=}"
;;
start|stop|status|restart|initialize|update|reload)
COMMON_CORE_ACTION="$1"
;;
-h|--help)
COMMON_CORE_HELP=1
;;
--)
shift
break
;;
*)
if [[ -z "$COMMON_CORE_ACTION" && "$1" != -* ]]; then
COMMON_CORE_ACTION="$1"
else
COMMON_CORE_PARSE_ERROR="Unknown argument: $1"
break
fi
;;
esac
shift
done
if [[ -z "$COMMON_CORE_ACTION" && "$COMMON_CORE_HELP" -eq 0 && -z "$COMMON_CORE_PARSE_ERROR" ]]; then
COMMON_CORE_PARSE_ERROR="Action is required (${COMMON_CORE_ACTIONS//|/, })"
fi
}
common_core_usage() {
local prog="${1:-$0}"
echo "Usage: ${prog##*/} [${COMMON_CORE_ACTIONS//|/|}] [-n namespace] [--mode k3d|k3s|k8s] [-c conf/prole.cfg]" >&2
}
# Resolve namespace with precedence: CLI override -> SERVICE_NAMESPACE -> NAMESPACE -> provided default
common_core_resolve_namespace() {
local default_ns="${1:-default}"
local ns="${COMMON_CORE_NAMESPACE:-}"
[[ -z "$ns" && -n "${SERVICE_NAMESPACE:-}" ]] && ns="$SERVICE_NAMESPACE"
[[ -z "$ns" && -n "${NAMESPACE:-}" ]] && ns="$NAMESPACE"
[[ -z "$ns" ]] && ns="$default_ns"
echo "$ns"
}
# Apply a resolved namespace to both NAMESPACE and SERVICE_NAMESPACE for consistency
common_core_apply_namespace() {
local ns="$1"
if [[ -n "$ns" ]]; then
NAMESPACE="$ns"
SERVICE_NAMESPACE="$ns"
export NAMESPACE SERVICE_NAMESPACE
fi
}

2
mock_val/etc/home.sh Executable file
View File

@ -0,0 +1,2 @@
#!/bin/bash
echo hometest

2
mock_val/home.sh Executable file
View File

@ -0,0 +1,2 @@
#!/bin/bash
echo hometest

170
mock_val/init_argocd.sh Executable file
View File

@ -0,0 +1,170 @@
#!/usr/bin/env bash
set -euo pipefail
# init_argocd.sh
# Deploy and manage ArgoCD as part of the common core stack.
SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
# Shared option parsing for common core scripts
# shellcheck disable=SC1090
source "$SCRIPT_DIR/common_core_lib.sh"
common_core_preparse_config "$@"
# shellcheck disable=SC1090
source "$SCRIPT_DIR/prole_cfg.sh"
set -- "${COMMON_CORE_ARGS[@]}"
common_core_parse_args "$@"
if [[ "${COMMON_CORE_HELP:-0}" == 1 ]]; then
common_core_usage "$0"
exit 0
fi
if [[ -n "${COMMON_CORE_PARSE_ERROR:-}" ]]; then
echo "ERROR: ${COMMON_CORE_PARSE_ERROR}" >&2
common_core_usage "$0"
exit 2
fi
ACTION="$COMMON_CORE_ACTION"
if [[ -d "$SCRIPT_DIR/../k8s/argocd" ]]; then
ARGOCD_MANIFEST_DIR="$SCRIPT_DIR/../k8s/argocd"
elif [[ -n "${PROLE_HOME:-}" && -d "$PROLE_HOME/k8s/argocd" ]]; then
ARGOCD_MANIFEST_DIR="$PROLE_HOME/k8s/argocd"
else
ARGOCD_MANIFEST_DIR="$SCRIPT_DIR/../k8s/argocd"
fi
ARGOCD_MANIFEST_FILE=${ARGOCD_MANIFEST_FILE:-"$ARGOCD_MANIFEST_DIR/install.yaml"}
ARGOCD_NAMESPACE_DEFAULT="${ARGOCD_NAMESPACE:-argocd}"
ARGOCD_NAMESPACE="$(common_core_resolve_namespace "$ARGOCD_NAMESPACE_DEFAULT")"
common_core_apply_namespace "$ARGOCD_NAMESPACE"
export ARGOCD_NAMESPACE
ARGOCD_LABEL_SELECTOR=${ARGOCD_LABEL_SELECTOR:-"app.kubernetes.io/part-of=argocd"}
ARGOCD_SERVER_SERVICE=${ARGOCD_SERVER_SERVICE:-argocd-server}
ARGOCD_PORT_FORWARD_LOCAL=${ARGOCD_PORT_FORWARD_LOCAL:-8081}
ARGOCD_PORT_FORWARD_REMOTE=${ARGOCD_PORT_FORWARD_REMOTE:-80}
ARGOCD_NODE_SELECTOR=${ARGOCD_NODE_SELECTOR:-}
ensure_tools() {
command -v kubectl >/dev/null || { echo "Missing required tool: kubectl" >&2; exit 1; }
}
ensure_namespace() {
if ! kubectl get namespace "$ARGOCD_NAMESPACE" >/dev/null 2>&1; then
echo "Creating namespace '$ARGOCD_NAMESPACE' ..."
kubectl create namespace "$ARGOCD_NAMESPACE" >/dev/null 2>&1 || true
fi
}
render_manifest() {
if [[ ! -f "$ARGOCD_MANIFEST_FILE" ]]; then
echo "ERROR: ArgoCD manifest not found: $ARGOCD_MANIFEST_FILE" >&2
exit 1
fi
prole_render_manifest "$ARGOCD_MANIFEST_FILE" | sed "s|\\${ARGOCD_NAMESPACE}|$ARGOCD_NAMESPACE|g"
}
resolve_argocd_node_selector() {
if [[ -n "$ARGOCD_NODE_SELECTOR" ]]; then
printf '%s' "$ARGOCD_NODE_SELECTOR"
return 0
fi
local node=""
node=$(kubectl get nodes -l 'node-role.kubernetes.io/control-plane' -o jsonpath='{.items[0].metadata.name}' 2>/dev/null || true)
if [[ -n "$node" ]]; then
printf 'kubernetes.io/hostname=%s' "$node"
fi
}
apply_argocd_node_selector() {
local selector
selector="$(resolve_argocd_node_selector)"
if [[ -z "$selector" ]]; then
return 0
fi
local key="${selector%%=*}"
local val="${selector#*=}"
if [[ -z "$key" || -z "$val" ]]; then
echo "WARN: invalid ARGOCD_NODE_SELECTOR '$selector' (expected key=value); skipping." >&2
return 0
fi
for target in \
deployment/argocd-applicationset-controller \
deployment/argocd-dex-server \
deployment/argocd-notifications-controller \
deployment/argocd-repo-server \
deployment/argocd-server \
deployment/argocd-redis \
statefulset/argocd-application-controller; do
kubectl -n "$ARGOCD_NAMESPACE" patch "$target" \
--type merge \
-p "{\"spec\":{\"template\":{\"spec\":{\"nodeSelector\":{\"$key\":\"$val\"}}}}}" >/dev/null 2>&1 || true
done
}
apply_argocd() {
echo "Applying ArgoCD manifest to namespace '$ARGOCD_NAMESPACE' ..."
render_manifest | kubectl apply --server-side --force-conflicts --field-manager=prole-installer --validate=false -n "$ARGOCD_NAMESPACE" -f -
apply_argocd_node_selector
kubectl rollout status deploy/argocd-server -n "$ARGOCD_NAMESPACE" --timeout=${ROLLOUT_TIMEOUT:-300s} || true
kubectl rollout status deploy/argocd-repo-server -n "$ARGOCD_NAMESPACE" --timeout=${ROLLOUT_TIMEOUT:-300s} || true
kubectl rollout status deploy/argocd-dex-server -n "$ARGOCD_NAMESPACE" --timeout=${ROLLOUT_TIMEOUT:-300s} || true
kubectl rollout status deploy/argocd-applicationset-controller -n "$ARGOCD_NAMESPACE" --timeout=${ROLLOUT_TIMEOUT:-300s} || true
kubectl rollout status deploy/argocd-notifications-controller -n "$ARGOCD_NAMESPACE" --timeout=${ROLLOUT_TIMEOUT:-300s} || true
kubectl rollout status deploy/argocd-redis -n "$ARGOCD_NAMESPACE" --timeout=${ROLLOUT_TIMEOUT:-300s} || true
kubectl rollout status statefulset/argocd-application-controller -n "$ARGOCD_NAMESPACE" --timeout=${ROLLOUT_TIMEOUT:-300s} || true
}
rollout_restart_argocd() {
kubectl -n "$ARGOCD_NAMESPACE" rollout restart deploy/argocd-server deploy/argocd-repo-server \
deploy/argocd-dex-server deploy/argocd-applicationset-controller deploy/argocd-notifications-controller \
deploy/argocd-redis >/dev/null 2>&1 || true
kubectl -n "$ARGOCD_NAMESPACE" rollout restart statefulset/argocd-application-controller >/dev/null 2>&1 || true
}
delete_argocd() {
echo "Removing ArgoCD resources from namespace '$ARGOCD_NAMESPACE' ..."
render_manifest | kubectl delete -n "$ARGOCD_NAMESPACE" -f - --ignore-not-found
}
status_argocd() {
kubectl -n "$ARGOCD_NAMESPACE" get deploy,statefulset,svc -l "$ARGOCD_LABEL_SELECTOR" 2>/dev/null || true
kubectl -n "$ARGOCD_NAMESPACE" get pods -l "$ARGOCD_LABEL_SELECTOR" 2>/dev/null || true
}
case "$ACTION" in
start|initialize|update|reload)
ensure_tools
ensure_namespace
apply_argocd
prole_register_port_forward "argocd" "$ARGOCD_NAMESPACE" "svc/${ARGOCD_SERVER_SERVICE}" \
"$ARGOCD_PORT_FORWARD_LOCAL" "$ARGOCD_PORT_FORWARD_REMOTE" "0.0.0.0" "TCP" "ArgoCD"
;;
restart)
ensure_tools
ensure_namespace
apply_argocd
rollout_restart_argocd
prole_register_port_forward "argocd" "$ARGOCD_NAMESPACE" "svc/${ARGOCD_SERVER_SERVICE}" \
"$ARGOCD_PORT_FORWARD_LOCAL" "$ARGOCD_PORT_FORWARD_REMOTE" "0.0.0.0" "TCP" "ArgoCD"
;;
stop)
ensure_tools
delete_argocd
;;
status)
ensure_tools
status_argocd
;;
*)
common_core_usage "$0"
exit 1
;;
esac

154
mock_val/init_certmgr.sh Executable file
View File

@ -0,0 +1,154 @@
#!/usr/bin/env bash
set -euo pipefail
# init_certmgr.sh
# Purpose:
# - Install or manage cert-manager for ACME certificates
# - Provide start/stop/status/restart/update actions
SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
# Shared option parsing for common core scripts
# shellcheck disable=SC1090
source "$SCRIPT_DIR/common_core_lib.sh"
# Inject default config if not provided
_has_config=0
for _arg in "$@"; do
[[ "$_arg" == "-c" || "$_arg" == "--config" || "$_arg" == -c=* || "$_arg" == --config=* ]] && _has_config=1
done
if [[ $_has_config -eq 0 && -f "$SCRIPT_DIR/../conf/prole.cfg" ]]; then
set -- "-c" "$SCRIPT_DIR/../conf/prole.cfg" "$@"
fi
unset _has_config _arg
common_core_preparse_config "$@"
# shellcheck disable=SC1090
source "$SCRIPT_DIR/prole_cfg.sh"
set -- "${COMMON_CORE_ARGS[@]}"
common_core_parse_args "$@"
if [[ -z "${PROLE_MODE:-}" ]]; then
export PROLE_MODE="k3s"
fi
if [[ "${COMMON_CORE_HELP:-0}" == 1 ]]; then
common_core_usage "$0"
exit 0
fi
if [[ -n "${COMMON_CORE_PARSE_ERROR:-}" ]]; then
echo "ERROR: ${COMMON_CORE_PARSE_ERROR}" >&2
common_core_usage "$0"
exit 2
fi
ACTION="$COMMON_CORE_ACTION"
# cert-manager always runs in its own dedicated namespace; never follows the
# common-services namespace override.
CERTMGR_NAMESPACE="cert-manager"
common_core_apply_namespace "$CERTMGR_NAMESPACE"
CERTMGR_VERSION="${CERTMGR_VERSION:-v1.14.6}"
CERTMGR_RELEASE_URL="https://github.com/cert-manager/cert-manager/releases/download/${CERTMGR_VERSION}/cert-manager.yaml"
CERTMGR_PF_ADDR="${CERTMGR_PF_ADDR:-127.0.0.1}"
if [[ "${PROLE_MODE:-}" == "k3s" ]]; then
CERTMGR_PF_ADDR="${CERTMGR_PF_ADDR:-0.0.0.0}"
fi
usage() {
cat <<USAGE
Usage: $0 [--mode MODE] [-n NAMESPACE] [${COMMON_CORE_ACTIONS//|/|}] [-c conf/prole.cfg]
Actions:
start Install cert-manager (CRDs + controller)
stop Remove cert-manager resources
status Show cert-manager deployment status
restart Restart cert-manager deployments
update Install or update cert-manager resources
USAGE
exit 1
}
ensure_tools() {
for t in kubectl curl; do
command -v "$t" >/dev/null || { echo "Missing required tool: $t" >&2; exit 1; }
done
}
ensure_namespace() {
if ! kubectl get namespace "$CERTMGR_NAMESPACE" >/dev/null 2>&1; then
echo "Creating namespace '$CERTMGR_NAMESPACE' ..."
kubectl create namespace "$CERTMGR_NAMESPACE" >/dev/null 2>&1 || true
fi
}
_apply_manifest() {
curl -fsSL "$CERTMGR_RELEASE_URL" | kubectl apply -f -
}
_is_already_deployed() {
kubectl -n "$CERTMGR_NAMESPACE" get deployment cert-manager cert-manager-cainjector cert-manager-webhook >/dev/null 2>&1
}
_delete_manifest() {
curl -fsSL "$CERTMGR_RELEASE_URL" | kubectl delete -f - --ignore-not-found=true
}
wait_rollout() {
kubectl -n "$CERTMGR_NAMESPACE" rollout status deployment/cert-manager --timeout=180s
kubectl -n "$CERTMGR_NAMESPACE" rollout status deployment/cert-manager-cainjector --timeout=180s
kubectl -n "$CERTMGR_NAMESPACE" rollout status deployment/cert-manager-webhook --timeout=180s
}
status() {
echo "=== cert-manager deployments (${CERTMGR_NAMESPACE}) ==="
kubectl -n "$CERTMGR_NAMESPACE" get deploy cert-manager cert-manager-cainjector cert-manager-webhook 2>/dev/null || echo "No deployments found"
echo ""
echo "=== cert-manager pods (${CERTMGR_NAMESPACE}) ==="
kubectl -n "$CERTMGR_NAMESPACE" get pods 2>/dev/null || echo "No pods found"
}
restart() {
echo "Restarting cert-manager deployments in namespace '$CERTMGR_NAMESPACE' ..."
kubectl -n "$CERTMGR_NAMESPACE" rollout restart deployment/cert-manager
kubectl -n "$CERTMGR_NAMESPACE" rollout restart deployment/cert-manager-cainjector
kubectl -n "$CERTMGR_NAMESPACE" rollout restart deployment/cert-manager-webhook
wait_rollout
}
action_update() {
ensure_tools
ensure_namespace
if _is_already_deployed; then
echo "cert-manager already deployed in namespace '$CERTMGR_NAMESPACE'; skipping manifest apply."
else
_apply_manifest
wait_rollout
fi
prole_register_port_forward "certmgr" "$CERTMGR_NAMESPACE" "svc/cert-manager-webhook" "9443" "9443" "${CERTMGR_PF_ADDR}" "TCP" "CertManager Webhook"
}
case "$ACTION" in
start|initialize|update|reload)
action_update
;;
stop)
ensure_tools
_delete_manifest
;;
status)
ensure_tools
status
;;
restart)
ensure_tools
restart
;;
*)
usage
;;
esac

View File

@ -204,8 +204,8 @@ openbao_url() {
if curl -sS "http://127.0.0.1:8200/v1/sys/health" >/dev/null 2>&1; then if curl -sS "http://127.0.0.1:8200/v1/sys/health" >/dev/null 2>&1; then
echo "http://127.0.0.1:8200" echo "http://127.0.0.1:8200"
return 0 return 0
elif curl -sS "http://127.0.0.1:18200/v1/sys/health" >/dev/null 2>&1; then elif curl -sS "http://127.0.0.1:8200/v1/sys/health" >/dev/null 2>&1; then
echo "http://127.0.0.1:18200" echo "http://127.0.0.1:8200"
return 0 return 0
else else
echo "" echo ""
@ -577,6 +577,137 @@ apply_prole_manifest_file() {
return 1 return 1
} }
# Push image to local registry (via LOCAL_REGISTRY host address) with k3d import fallback.
_push_to_k3d_registry() {
local image="$1"
local cluster_name="$2"
local push_host="${LOCAL_REGISTRY:-localhost:5000}"
local plain_image="${image##*/}" # strip registry prefix, e.g. prole-db:18-088
if [[ -n "$push_host" ]]; then
local push_ref="${push_host}/${plain_image}"
docker tag "$image" "$push_ref" 2>/dev/null || true
if docker push "$push_ref" 2>/dev/null; then
echo " ✓ Image pushed to registry at '${push_host}'."
return 0
fi
echo " WARN: push to '${push_host}' failed; falling back to k3d image import ..." >&2
fi
if k3d image import "$image" -c "$cluster_name" 2>/dev/null; then
echo " ✓ Image '$image' imported directly into k3d cluster '$cluster_name'."
return 0
fi
echo "ERROR: Failed to push or import image '$image'." >&2
return 1
}
# Pre-flight: ensure the prole-db image is available in the k3d cluster before
# the CNPG operator ever tries to pull it, avoiding ErrImagePull backoff loops.
# Steps: containerd cache → Docker daemon (registry tag) → Docker daemon (plain tag)
# → tar import → docker build + push/import.
_ensure_prole_db_image() {
[[ "${PROLE_MODE:-}" != "k3d" ]] && return 0
local image_override="${CNPG_IMAGE:-${PROLE_DB_IMAGE:-}}"
local image=""
if [[ -n "$image_override" ]]; then
image="$image_override"
else
if [[ "$VERSION" == "latest" || -z "$VERSION" ]]; then
image=$(get_latest_image)
else
image="prole-db:$VERSION"
fi
fi
image=$(resolve_cnpg_image "$image")
# Auto-detect active k3d cluster name
local cluster_name="${K3D_CLUSTER_NAME:-}"
if [[ -z "$cluster_name" ]]; then
cluster_name=$(k3d cluster list --no-headers 2>/dev/null | awk '{print $1}' | head -1)
fi
cluster_name="${cluster_name:-prole-dev-cluster}"
local prole_db_dir="${PROLE_HOME:-$SCRIPT_DIR/..}/prole-db"
local plain_image="${image##*/}" # e.g. prole-db:18-088
echo "Pre-flight: verifying image '$image' is available in k3d cluster '$cluster_name' ..."
# Step 1: check if already present in k3d containerd with matching digest
local containerd_digest local_digest containerd_sha
containerd_digest=$(docker exec "k3d-${cluster_name}-server-0" \
ctr images ls -q 2>/dev/null | grep -F "$image" | head -1 || true)
if [[ -n "$containerd_digest" ]]; then
local_digest=$(docker inspect --format='{{index .RepoDigests 0}}' "$image" 2>/dev/null \
| awk -F@ '{print $2}' || true)
containerd_sha=$(docker exec "k3d-${cluster_name}-server-0" \
ctr images ls 2>/dev/null | grep -F "$image" | awk '{print $3}' | head -1 || true)
if [[ -z "$local_digest" || "$containerd_sha" == "$local_digest" ]]; then
echo " ✓ Image '$image' already in k3d containerd (digest match); no import needed."
return 0
fi
echo " Image '$image' in containerd but digest mismatch (local: ${local_digest:-unknown}, containerd: ${containerd_sha:-unknown}); re-importing ..."
docker exec "k3d-${cluster_name}-server-0" ctr images rm "$image" 2>/dev/null || true
fi
# Step 2a: registry-tagged image in local Docker daemon → push + import
if docker image inspect "$image" >/dev/null 2>&1; then
echo " Image '$image' found in Docker daemon; pushing to registry ..."
_push_to_k3d_registry "$image" "$cluster_name"
return $?
fi
# Step 2b: plain-tagged image in local Docker daemon → tag + push + import
if docker image inspect "$plain_image" >/dev/null 2>&1; then
echo " Plain image '$plain_image' found in Docker daemon; tagging as '$image' and pushing ..."
docker tag "$plain_image" "$image"
_push_to_k3d_registry "$image" "$cluster_name"
return $?
fi
# Step 3: look for a matching tar in the docker-import directory
local prole_data="${PROLE_DATA:-$HOME/.prole/data}"
local docker_import_dir="${DOCKER_IMPORT_DIR:-${prole_data}/docker-import}"
local name_part="${plain_image%%:*}" # e.g. prole-db
local tag_part="${plain_image##*:}" # e.g. 18-088
local found_tar=""
if [[ -d "$docker_import_dir" ]]; then
for _t in "$docker_import_dir"/*.tar; do
[[ -f "$_t" ]] || continue
local _bn
_bn=$(basename "$_t" .tar)
if [[ "$_bn" == *"$name_part"* && "$_bn" == *"$tag_part"* ]]; then
found_tar="$_t"
break
fi
done
fi
if [[ -n "$found_tar" ]]; then
echo " Loading tar '$(basename "$found_tar")' into Docker daemon ..."
docker load -i "$found_tar"
docker tag "$plain_image" "$image" 2>/dev/null || true
_push_to_k3d_registry "$image" "$cluster_name"
return $?
fi
# Step 4: image not found anywhere — build from source then push + import
if [[ ! -f "$prole_db_dir/Dockerfile" ]]; then
echo "ERROR: Dockerfile not found in '$prole_db_dir'; cannot build prole-db image." >&2
return 1
fi
echo " Image '$image' not found in k3d, Docker daemon, or docker-import dir."
echo " Building prole-db image from '$prole_db_dir' ..."
if ! docker build -t "$plain_image" "$prole_db_dir"; then
echo "ERROR: docker build failed for image '$plain_image'." >&2
return 1
fi
docker tag "$plain_image" "$image"
echo " Build complete. Pushing '$image' to registry ..."
_push_to_k3d_registry "$image" "$cluster_name"
return $?
}
ensure_prole_stack_resources() { ensure_prole_stack_resources() {
echo "Applying CloudNative-PG cluster and related resources ..." echo "Applying CloudNative-PG cluster and related resources ..."
local image_override="${CNPG_IMAGE:-${PROLE_DB_IMAGE:-}}" local image_override="${CNPG_IMAGE:-${PROLE_DB_IMAGE:-}}"
@ -726,6 +857,7 @@ wait_for_cnpg_pods() {
echo " Timeout: ${timeout}s" echo " Timeout: ${timeout}s"
local last_feedback=0 local last_feedback=0
local feedback_interval=30 # print detailed status every 30s local feedback_interval=30 # print detailed status every 30s
local _image_pull_remediated="" # set after first k3d import attempt to avoid loops
while true; do while true; do
local now elapsed local now elapsed
now=$(date +%s) now=$(date +%s)
@ -735,13 +867,47 @@ wait_for_cnpg_pods() {
pods=$(kubectl -n "$NAMESPACE" get pods -l "cnpg.io/cluster=$CNPG_CLUSTER_NAME" --no-headers 2>/dev/null || true) pods=$(kubectl -n "$NAMESPACE" get pods -l "cnpg.io/cluster=$CNPG_CLUSTER_NAME" --no-headers 2>/dev/null || true)
if [[ -n "$pods" ]]; then if [[ -n "$pods" ]]; then
# Check for Error or CrashLoopBackOff # Check for Error or CrashLoopBackOff (terminal failures)
if echo "$pods" | grep -E "Error|CrashLoopBackOff" >/dev/null; then if echo "$pods" | grep -E "Error|CrashLoopBackOff" >/dev/null; then
echo "ERROR: Some CNPG pods are in Error or CrashLoopBackOff state:" >&2 echo "ERROR: Some CNPG pods are in Error or CrashLoopBackOff state:" >&2
echo "$pods" | grep -E "Error|CrashLoopBackOff" >&2 echo "$pods" | grep -E "Error|CrashLoopBackOff" >&2
return 1 return 1
fi fi
# Check for image pull failures — attempt remediation in k3d mode
if echo "$pods" | grep -E "ErrImagePull|ImagePullBackOff" >/dev/null; then
if [[ "${PROLE_MODE:-}" == "k3d" ]]; then
if [[ -z "$_image_pull_remediated" ]]; then
_image_pull_remediated=1
local fail_pod fail_image
fail_pod=$(echo "$pods" | grep -E "ErrImagePull|ImagePullBackOff" | awk '{print $1}' | head -1)
# Try containers then initContainers
fail_image=$(kubectl -n "$NAMESPACE" get pod "$fail_pod" \
-o jsonpath='{.spec.containers[0].image}' 2>/dev/null || true)
if [[ -z "$fail_image" ]]; then
fail_image=$(kubectl -n "$NAMESPACE" get pod "$fail_pod" \
-o jsonpath='{.spec.initContainers[0].image}' 2>/dev/null || true)
fi
echo "WARN: Pod '$fail_pod' cannot pull image '${fail_image:-unknown}' (ErrImagePull/ImagePullBackOff in k3d mode)." >&2
import_dir="${DOCKER_IMPORT_DIR:-${PROLE_DATA:+${PROLE_DATA}/docker-import}}"
cluster_name="${K3D_CLUSTER_NAME:-}"
if [[ -z "$cluster_name" ]]; then
cluster_name=$(k3d cluster list --no-headers 2>/dev/null | awk '{print $1}' | head -1)
fi
cluster_name="${cluster_name:-prole-dev-cluster}"
# Re-run full image ensure logic to push/import the failing image
echo " Attempting image remediation via _ensure_prole_db_image ..." >&2
_ensure_prole_db_image >&2 || true
echo " Remediation complete; resuming wait ..." >&2
fi
# Continue the wait loop — do not return 1
else
echo "ERROR: Image pull failure in non-k3d mode — cannot auto-recover:" >&2
echo "$pods" | grep -E "ErrImagePull|ImagePullBackOff" >&2
return 1
fi
fi
# Count Running pods by name (exclude initdb) # Count Running pods by name (exclude initdb)
local running_pods local running_pods
running_pods=$(kubectl -n "$NAMESPACE" get pods --no-headers 2>/dev/null \ running_pods=$(kubectl -n "$NAMESPACE" get pods --no-headers 2>/dev/null \
@ -1131,8 +1297,8 @@ bao_service_url() {
echo "http://$OPENBAO_NAME.${OPENBAO_NAMESPACE:-${SERVICE_NAMESPACE:-default}}.svc.cluster.local:8200" echo "http://$OPENBAO_NAME.${OPENBAO_NAMESPACE:-${SERVICE_NAMESPACE:-default}}.svc.cluster.local:8200"
return 0 return 0
fi fi
if curl -sS "http://127.0.0.1:18200/v1/sys/health" >/dev/null 2>&1; then if curl -sS "http://127.0.0.1:8200/v1/sys/health" >/dev/null 2>&1; then
echo "http://127.0.0.1:18200" echo "http://127.0.0.1:8200"
return 0 return 0
fi fi
if curl -sS "http://127.0.0.1:8200/v1/sys/health" >/dev/null 2>&1; then if curl -sS "http://127.0.0.1:8200/v1/sys/health" >/dev/null 2>&1; then
@ -1432,6 +1598,11 @@ initialize() {
fi fi
echo "Pre-flight check passed: all required secrets present." echo "Pre-flight check passed: all required secrets present."
if ! _ensure_prole_db_image; then
echo "ERROR: Pre-flight image check failed; aborting cluster initialization." >&2
return 1
fi
ensure_prole_stack_resources ensure_prole_stack_resources
if ! wait_for_cnpg_pods 300; then if ! wait_for_cnpg_pods 300; then
@ -1486,6 +1657,7 @@ deploy_cluster() {
generate_tls_if_missing generate_tls_if_missing
echo "Deploying $image to cluster $CNPG_CLUSTER_NAME in namespace $NAMESPACE..." echo "Deploying $image to cluster $CNPG_CLUSTER_NAME in namespace $NAMESPACE..."
_ensure_prole_db_image || true
apply_cnpg_cluster_manifest "$CNPG_MANIFEST" apply_cnpg_cluster_manifest "$CNPG_MANIFEST"
local current_image local current_image
@ -1503,6 +1675,20 @@ case "$ACTION" in
recreate) recreate)
ensure_tools ensure_tools
"$0" delete "$CNPG_CLUSTER_NAME" "$0" delete "$CNPG_CLUSTER_NAME"
# Clean up PVCs left behind by the CNPG operator (not in static manifests)
echo "Cleaning up PVCs in namespace '$NAMESPACE' ..."
kubectl -n "$NAMESPACE" delete pvc --all --ignore-not-found 2>/dev/null || true
# Wait briefly for pods to fully terminate before re-creating
echo "Waiting for pods to terminate in namespace '$NAMESPACE' ..."
_wait_term=0
while kubectl -n "$NAMESPACE" get pods --no-headers 2>/dev/null | grep -qv '^No resources'; do
sleep 3
_wait_term=$(( _wait_term + 3 ))
if (( _wait_term >= 60 )); then
echo "WARN: Pods still present after 60s; proceeding anyway." >&2
break
fi
done
"$0" create "$CNPG_CLUSTER_NAME" "$0" create "$CNPG_CLUSTER_NAME"
;; ;;
create) create)
@ -1513,7 +1699,20 @@ case "$ACTION" in
delete) delete)
ensure_tools ensure_tools
echo "Deleting all resources for '$CNPG_CLUSTER_NAME' ..." echo "Deleting all resources for '$CNPG_CLUSTER_NAME' ..."
kubectl delete -n "$NAMESPACE" -k "$SCRIPT_DIR/../k8s/prole" --ignore-not-found # Delete each manifest individually, mirroring the apply pattern:
# ingress.yaml contains multi-namespace resources and must be deleted without -n.
for _del_f in "$SCRIPT_DIR/../k8s/prole"/*.yaml; do
_del_base=$(basename "$_del_f")
case "$_del_base" in
kustomization.yaml|ingress.yaml) continue ;;
*) kubectl delete -n "$NAMESPACE" -f "$_del_f" --ignore-not-found 2>&1 \
| grep -v "^Error from server (NotFound)" || true ;;
esac
done
if [[ -f "$SCRIPT_DIR/../k8s/prole/ingress.yaml" ]]; then
kubectl delete -f "$SCRIPT_DIR/../k8s/prole/ingress.yaml" --ignore-not-found 2>&1 \
| grep -v "^Error from server (NotFound)" || true
fi
;; ;;
start) start)
ensure_tools ensure_tools

70
mock_val/init_common_core.sh Executable file
View File

@ -0,0 +1,70 @@
#!/usr/bin/env bash
set -euo pipefail
# init_common_core.sh
# Launch wrapper that runs all common core services (OpenBao, OpenTofu, Garage, ArgoCD)
# with a unified interface.
SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
# shellcheck disable=SC1090
source "$SCRIPT_DIR/common_core_lib.sh"
common_core_preparse_config "$@"
# shellcheck disable=SC1090
source "$SCRIPT_DIR/prole_cfg.sh"
set -- "${COMMON_CORE_ARGS[@]}"
common_core_parse_args "$@"
if [[ "${COMMON_CORE_HELP:-0}" == 1 ]]; then
common_core_usage "$0"
exit 0
fi
if [[ -n "${COMMON_CORE_PARSE_ERROR:-}" ]]; then
echo "ERROR: ${COMMON_CORE_PARSE_ERROR}" >&2
common_core_usage "$0"
exit 2
fi
ACTION="$COMMON_CORE_ACTION"
TARGET_NAMESPACE="$(common_core_resolve_namespace "default")"
common_core_apply_namespace "$TARGET_NAMESPACE"
# Forwarded option bundles for child scripts
NS_ARGS=("-n" "$TARGET_NAMESPACE")
if [[ -n "${COMMON_CORE_NAMESPACE:-}" ]]; then
NS_ARGS=("-n" "$COMMON_CORE_NAMESPACE")
fi
MODE_ARGS=()
if [[ -n "${PROLE_MODE:-}" ]]; then
MODE_ARGS=("--mode" "$PROLE_MODE")
fi
CFG_ARGS=()
if [[ -n "${COMMON_CORE_CONFIG_PATH:-}" ]]; then
CFG_ARGS=("-c" "$COMMON_CORE_CONFIG_PATH")
fi
COMPONENTS=(
"openbao"
"opentofu"
"garage_store"
"argocd"
)
for comp in "${COMPONENTS[@]}"; do
script="$SCRIPT_DIR/init_${comp}.sh"
if [[ ! -x "$script" ]]; then
echo "WARN: missing script $script; skipping." >&2
continue
fi
echo ">>> init_${comp}.sh ${ACTION} (namespace=${NS_ARGS[1]})"
if ! "$script" "$ACTION" "${NS_ARGS[@]}" "${MODE_ARGS[@]}" "${CFG_ARGS[@]}"; then
echo "ERROR: init_${comp}.sh ${ACTION} failed" >&2
exit 1
fi
done

View File

@ -2,14 +2,30 @@
set -u set -u
SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
# Shared option parsing for common core scripts
# shellcheck disable=SC1090
source "$SCRIPT_DIR/common_core_lib.sh"
# Inject default config if not provided
_has_config=0
for _arg in "$@"; do
[[ "$_arg" == "-c" || "$_arg" == "--config" || "$_arg" == -c=* || "$_arg" == --config=* ]] && _has_config=1
done
if [[ $_has_config -eq 0 && -f "$SCRIPT_DIR/../conf/prole.cfg" ]]; then
set -- "-c" "$SCRIPT_DIR/../conf/prole.cfg" "$@"
fi
unset _has_config _arg
common_core_preparse_config "$@"
# shellcheck disable=SC1090 # shellcheck disable=SC1090
source "$SCRIPT_DIR/prole_cfg.sh" source "$SCRIPT_DIR/prole_cfg.sh"
usage() { usage() {
cat <<EOF cat <<EOF
Usage: init_common_services.sh [-n|--namespace NS] [-k|--kerberos] <update|start|status> Usage: init_common_services.sh [-n|--namespace NS] [-k|--kerberos] <update|start|status|verify>
Deploys common infrastructure services (ArgoCD, OpenTofu, Garage) Deploys common infrastructure services (ArgoCD, OpenTofu, Garage, OpenBao, Kong, Cert-Manager)
into the given Kubernetes namespace. Use -k to include the Kerberos/KDC service. into the given Kubernetes namespace. Use -k to include the Kerberos/KDC service.
EOF EOF
} }
@ -20,6 +36,10 @@ ENABLE_KERBEROS=0
while [ $# -gt 0 ]; do while [ $# -gt 0 ]; do
case "$1" in case "$1" in
-c|--config)
shift
shift
;;
-m|--mode) -m|--mode)
shift shift
prole_set_mode "${1:-}" prole_set_mode "${1:-}"
@ -42,8 +62,9 @@ while [ $# -gt 0 ]; do
ENABLE_KERBEROS=1 ENABLE_KERBEROS=1
shift shift
;; ;;
update|start|status) update|start|status|verify|repair|reload|restart)
ACTION="$1" ACTION="$1"
[[ "$ACTION" == "repair" ]] && ACTION="update"
shift shift
;; ;;
-h|--help) -h|--help)
@ -69,6 +90,41 @@ REGISTRY_NS="${REGISTRY_NAMESPACE:-default}"
prole_ensure_kubeconfig >/dev/null 2>&1 || true prole_ensure_kubeconfig >/dev/null 2>&1 || true
# Check cluster reachability early to fail fast
if [[ "$ACTION" != "status" ]]; then
if ! kubectl cluster-info >/dev/null 2>&1; then
echo "ERROR: Cluster not reachable. Check KUBECONFIG and cluster status." >&2
exit 1
fi
fi
_verify_common_services() {
local status_script="$SCRIPT_DIR/status_common_services.sh"
if [[ ! -x "$status_script" ]]; then
echo "ERROR: status_common_services.sh not found at $status_script" >&2
return 2
fi
local args=(-n "$NS")
if [[ "$ENABLE_KERBEROS" == "1" ]]; then
args+=(-k)
fi
echo "Verifying common services in namespace '$NS' ..."
"$status_script" "${args[@]}"
}
# Short-circuit verify before any deploy/reload/preload work
if [[ -n "${COMMON_SERVICES_INIT_LOG:-}" ]]; then
mkdir -p "$(dirname "$COMMON_SERVICES_INIT_LOG")"
exec > >(tee -a "$COMMON_SERVICES_INIT_LOG") 2>&1
fi
if [[ "$ACTION" == "verify" ]]; then
_verify_common_services
exit $?
fi
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Pre-import container images from $PROLE_DATA/docker-import/*.tar # Pre-import container images from $PROLE_DATA/docker-import/*.tar
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@ -91,6 +147,80 @@ _resolve_prole_data() {
echo "${d:-}" echo "${d:-}"
} }
# Standard console execution element.
# Runs a command silently (output captured), displays a live elapsed-time footer
# on stderr when attached to a TTY, prints elapsed time on completion, and tails
# captured output for diagnostics on failure.
_exec_with_progress_footer() {
local label="$1"
shift
local start_time
start_time=$(date +%s)
local tmp_out
tmp_out=$(mktemp 2>/dev/null || echo "/tmp/_prole_exec_$$.out")
# Spawn live footer only when stderr is a real TTY
local spinner_pid=""
if [[ -t 2 ]]; then
(
while true; do
now=$(date +%s)
elapsed=$(( now - start_time ))
h=$(( elapsed / 3600 ))
m=$(( (elapsed % 3600) / 60 ))
s=$(( elapsed % 60 ))
printf "\r\033[2K [%02d:%02d:%02d elapsed] %s" "$h" "$m" "$s" "$label" >&2
sleep 1
done
) &
spinner_pid=$!
disown "$spinner_pid" 2>/dev/null || true
fi
"$@" >"$tmp_out" 2>&1
local rc=$?
if [[ -n "$spinner_pid" ]]; then
kill "$spinner_pid" 2>/dev/null || true
wait "$spinner_pid" 2>/dev/null || true
printf "\r\033[2K" >&2
fi
local end_time elapsed_total h m s
end_time=$(date +%s)
elapsed_total=$(( end_time - start_time ))
h=$(( elapsed_total / 3600 ))
m=$(( (elapsed_total % 3600) / 60 ))
s=$(( elapsed_total % 60 ))
printf " [%02dh:%02dm:%02ds elapsed] %s\n" "$h" "$m" "$s" "$label"
if [[ $rc -ne 0 && -s "$tmp_out" ]]; then
echo " [Last output]:" >&2
tail -n 10 "$tmp_out" >&2
fi
rm -f "$tmp_out"
return $rc
}
# Extract the primary image ref from a Docker tarball's manifest.json.
_image_name_from_tar() {
local tar_file="$1"
if command -v python3 >/dev/null 2>&1; then
python3 - "$tar_file" <<'PYEOF' 2>/dev/null
import json, sys, tarfile
try:
with tarfile.open(sys.argv[1]) as t:
tags = json.load(t.extractfile("manifest.json"))[0].get("RepoTags") or []
if tags:
print(tags[0])
except Exception:
pass
PYEOF
fi
}
_preload_docker_images() { _preload_docker_images() {
local prole_data local prole_data
prole_data=$(_resolve_prole_data) prole_data=$(_resolve_prole_data)
@ -118,54 +248,186 @@ _preload_docker_images() {
return 0 return 0
fi fi
local cluster_name="${K3D_CLUSTER_NAME:-prole-dev-cluster}" local cluster_name="${K3D_CLUSTER_NAME:-prole-dev-cluster}"
local tar_file
while IFS= read -r tar_file; do # ── Step 1: build indexed arrays of tar paths + resolved image names ──
echo " Importing $(basename "$tar_file") into k3d cluster '$cluster_name' ..." local -a tar_paths=()
if k3d image import "$tar_file" -c "$cluster_name" >/dev/null 2>&1; then local -a tar_images=()
echo " [OK] $(basename "$tar_file")" local _tf
else while IFS= read -r _tf; do
echo " [WARN] Failed to import $(basename "$tar_file"); image will be pulled at deploy time." >&2 [[ -z "$_tf" ]] && continue
fi tar_paths+=("$_tf")
tar_images+=("$(_image_name_from_tar "$_tf")")
done <<< "$tar_files" done <<< "$tar_files"
local total_tars=${#tar_paths[@]}
if [[ $total_tars -eq 0 ]]; then
echo " [SKIP] No tar files to process."
return 0
fi
# ── Step 2: query images already in the k3d cluster ───────────────────
echo " Checking k3d cluster '$cluster_name' for existing images ..."
local existing_images=""
local node_name="k3d-${cluster_name}-server-0"
if docker inspect "$node_name" >/dev/null 2>&1; then
existing_images=$(docker exec "$node_name" \
ctr --address /run/k3s/containerd/containerd.sock \
--namespace k8s.io images ls -q 2>/dev/null || true)
else
echo " [WARN] k3d node '$node_name' not reachable; will import all images."
fi
# ── Step 3: delta — split into already-present vs needs-import ─────────
local -a to_import_tars=()
local -a to_import_images=()
local -a already_present=()
local _i
for (( _i=0; _i<total_tars; _i++ )); do
local _tar="${tar_paths[$_i]}"
local _img="${tar_images[$_i]}"
local _found=0
if [[ -n "$_img" && -n "$existing_images" ]]; then
# Strip digest suffix before matching
local _img_ref="${_img%%@*}"
if echo "$existing_images" | grep -qF "$_img_ref"; then
_found=1
fi
fi
if [[ $_found -eq 1 ]]; then
already_present+=("$(basename "$_tar")${_img:+ → $_img}")
else
to_import_tars+=("$_tar")
to_import_images+=("${_img:-}")
fi
done
# ── Step 4: report delta to user ───────────────────────────────────────
if [[ ${#already_present[@]} -gt 0 ]]; then
echo " Already in cluster — skipping ${#already_present[@]} image(s):"
local _entry
for _entry in "${already_present[@]}"; do
echo "$_entry"
done
fi
local import_count=${#to_import_tars[@]}
if [[ $import_count -eq 0 ]]; then
echo " [OK] All images already present in k3d cluster '$cluster_name'; nothing to import."
return 0
fi
echo " Bulk uploading $import_count image(s) into k3d cluster '$cluster_name':"
for (( _i=0; _i<import_count; _i++ )); do
local _lbl="${to_import_images[$_i]:-$(basename "${to_import_tars[$_i]}")}"
echo "$(basename "${to_import_tars[$_i]}")${_lbl:+ ($_lbl)}"
done
# ── Step 5: bulk import with live progress footer ──────────────────────
local overall_start elapsed_bulk
overall_start=$(date +%s)
if _exec_with_progress_footer \
"k3d image import — bulk ($import_count images) → $cluster_name" \
k3d image import "${to_import_tars[@]}" -c "$cluster_name"; then
elapsed_bulk=$(( $(date +%s) - overall_start ))
local bm=$(( elapsed_bulk / 60 )) bs=$(( elapsed_bulk % 60 ))
echo " [OK] Bulk import of $import_count image(s) complete (${bm}m${bs}s)."
else
echo " [WARN] Bulk import failed; falling back to sequential import." >&2
local _idx
for (( _i=0; _i<import_count; _i++ )); do
_idx=$(( _i + 1 ))
local _tar="${to_import_tars[$_i]}"
local _img="${to_import_images[$_i]:-$(basename "$_tar")}"
# ETA based on average time per completed image
local _now _elapsed_so_far _eta_str=""
_now=$(date +%s)
_elapsed_so_far=$(( _now - overall_start ))
if [[ $_i -gt 0 ]]; then
local _avg=$(( _elapsed_so_far / _i ))
local _remaining=$(( _avg * (import_count - _i) ))
local _rm=$(( _remaining / 60 )) _rs=$(( _remaining % 60 ))
_eta_str=" | ETA ~${_rm}m${_rs}s"
fi
echo " [$_idx/$import_count] $(basename "$_tar")${_img:+ ($_img)}${_eta_str}"
local _item_start
_item_start=$(date +%s)
if _exec_with_progress_footer \
"k3d image import [$_idx/$import_count] $(basename "$_tar")" \
k3d image import "$_tar" -c "$cluster_name"; then
local _item_elapsed=$(( $(date +%s) - _item_start ))
echo " [OK] $(basename "$_tar") (${_item_elapsed}s)"
else
echo " [WARN] Failed to import $(basename "$_tar"); will pull at deploy time." >&2
fi
done
local _total_elapsed=$(( $(date +%s) - overall_start ))
local _te_m=$(( _total_elapsed / 60 )) _te_s=$(( _total_elapsed % 60 ))
echo " [DONE] Sequential import complete. Total: ${_te_m}m${_te_s}s"
fi
;; ;;
k3s) k3s)
# For k3s running from macOS: load tarballs into Docker, then push to # For k3s: copy tarballs directly to k3s nodes and import via
# the in-cluster registry:2. Falls back to Ansible ctr import if the # `k3s ctr images import`. This avoids the slow docker-push-to-registry
# registry push fails. # flow that can stall for minutes per image layer.
local registry="${LOCAL_REGISTRY:-}" local playbook_dir="$SCRIPT_DIR/../infrastructure/playbooks"
if [[ -z "$registry" ]]; then local playbook="$playbook_dir/k3s_import_images.yml"
# Derive from k3s server URL
local _srv="${PROLE_K3S_SERVER:-}" if [[ -f "$playbook" ]]; then
if [[ -n "$_srv" ]]; then # Build JSON array of objects for Ansible extra-vars
_srv="${_srv#https://}"; _srv="${_srv#http://}"; _srv="${_srv%%/*}"; _srv="${_srv%%:*}" local images_json="["
[[ -n "$_srv" ]] && registry="${_srv}:5000" local first=1
fi local tar_file
fi while IFS= read -r tar_file; do
local tar_file [[ -z "$tar_file" ]] && continue
while IFS= read -r tar_file; do
local base_name # Extract image name from tarball manifest on host
base_name=$(basename "$tar_file" .tar) local img_name=""
echo " Loading $(basename "$tar_file") into local Docker ..." if command -v python3 >/dev/null 2>&1; then
local loaded_image="" img_name=$(python3 -c "import json, tarfile;
loaded_image=$(docker load -i "$tar_file" 2>/dev/null | sed -n 's/^Loaded image: //p' || true) try:
if [[ -z "$loaded_image" ]]; then with tarfile.open('$tar_file') as tar:
# Try to derive image name from tarball filename (safe_name convention) m = json.load(tar.extractfile('manifest.json'))
loaded_image="${base_name//_//}" print(m[0]['RepoTags'][0])
loaded_image="${loaded_image%/*}:${loaded_image##*/}" except Exception:
fi pass" 2>/dev/null || true)
if [[ -n "$registry" && -n "$loaded_image" ]]; then
local remote_tag="${registry}/${loaded_image}"
docker tag "$loaded_image" "$remote_tag" 2>/dev/null || true
echo " Pushing $remote_tag to registry ..."
if docker push "$remote_tag" 2>/dev/null; then
echo " [OK] $(basename "$tar_file")$remote_tag"
continue
fi fi
echo " [WARN] Registry push failed; image will be pulled at deploy time." >&2
else # Fallback to safe_name derivation if python failed or not available
echo " [WARN] No registry configured; image will be pulled at deploy time." >&2 if [[ -z "$img_name" ]]; then
local base_name=$(basename "$tar_file" .tar)
img_name="${base_name//_//}"
img_name="${img_name%/*}:${img_name##*/}"
fi
[[ $first -eq 0 ]] && images_json+=","
images_json+="{\"path\": \"$tar_file\", \"image\": \"$img_name\"}"
first=0
echo " Queued: $(basename "$tar_file") ${img_name:+(image: $img_name)}"
done <<< "$tar_files"
images_json+="]"
echo " Importing images into k3s nodes via Ansible ..."
local ansible_cmd=(ansible-playbook)
local vault_pass="$SCRIPT_DIR/../.vault_pass"
if [[ -f "$vault_pass" ]]; then
ansible_cmd+=(--vault-password-file "$vault_pass")
elif [[ -n "${ANSIBLE_VAULT_PASSWORD_FILE:-}" ]]; then
ansible_cmd+=(--vault-password-file "$ANSIBLE_VAULT_PASSWORD_FILE")
fi fi
done <<< "$tar_files" ansible_cmd+=(-e "{\"k3s_import_images\": $images_json}")
ansible_cmd+=("$playbook")
if "${ansible_cmd[@]}"; then
echo " [OK] All images checked/imported into k3s nodes."
else
echo " [WARN] Ansible import failed; images will be pulled at deploy time." >&2
fi
else
echo " [WARN] Ansible playbook not found ($playbook); images will be pulled at deploy time." >&2
fi
;; ;;
*) *)
echo "WARN: Unknown mode '$mode'; skipping docker-import pre-load." >&2 echo "WARN: Unknown mode '$mode'; skipping docker-import pre-load." >&2
@ -176,7 +438,7 @@ _preload_docker_images() {
} }
case "$ACTION" in case "$ACTION" in
start|update|reload|initialize|restart) start|update|reload|initialize|restart|repair)
_preload_docker_images _preload_docker_images
;; ;;
esac esac
@ -190,12 +452,9 @@ if [ -x "$SCRIPT_DIR/init_service_layer.sh" ]; then
exit $? exit $?
fi fi
if [[ -n "${COMMON_SERVICES_INIT_LOG:-}" ]]; then
mkdir -p "$(dirname "$COMMON_SERVICES_INIT_LOG")"
exec > >(tee -a "$COMMON_SERVICES_INIT_LOG") 2>&1
fi
OPENTOFU_NAME=${OPENTOFU_NAME:-opentofu} OPENTOFU_NAME=${OPENTOFU_NAME:-opentofu}
OPENBAO_NAME=${OPENBAO_NAME:-openbao}
ARGOCD_SERVER_NAME=${ARGOCD_SERVER_NAME:-argocd-server} ARGOCD_SERVER_NAME=${ARGOCD_SERVER_NAME:-argocd-server}
GARAGE_NAME=${GARAGE_NAME:-garage} GARAGE_NAME=${GARAGE_NAME:-garage}
OPENTOFU_CONFIGMAP=${OPENTOFU_CONFIGMAP:-opentofu-nginx} OPENTOFU_CONFIGMAP=${OPENTOFU_CONFIGMAP:-opentofu-nginx}
@ -256,6 +515,17 @@ migrate_common_services() {
kubectl delete -n "$old_ns" configmap "$GARAGE_CONFIGMAP" --ignore-not-found >/dev/null 2>&1 || true kubectl delete -n "$old_ns" configmap "$GARAGE_CONFIGMAP" --ignore-not-found >/dev/null 2>&1 || true
kubectl delete -n "$old_ns" secret "$GARAGE_SECRET_NAME" --ignore-not-found >/dev/null 2>&1 || true kubectl delete -n "$old_ns" secret "$GARAGE_SECRET_NAME" --ignore-not-found >/dev/null 2>&1 || true
done done
for old_ns in $(collect_other_namespaces "$NS" "$OPENBAO_NAME" deployment service statefulset); do
echo "Found OpenBao in namespace '$old_ns'; removing before deploy to '$NS' ..."
if [ -x "$SCRIPT_DIR/init_openbao.sh" ]; then
"$SCRIPT_DIR/init_openbao.sh" -n "$old_ns" stop || true
else
kubectl delete -n "$old_ns" deploy "$OPENBAO_NAME" --ignore-not-found >/dev/null 2>&1 || true
kubectl delete -n "$old_ns" statefulset "$OPENBAO_NAME" --ignore-not-found >/dev/null 2>&1 || true
kubectl delete -n "$old_ns" svc "$OPENBAO_NAME" --ignore-not-found >/dev/null 2>&1 || true
fi
done
} }
echo "Deploying common services (namespace=$NS, action=$ACTION)" echo "Deploying common services (namespace=$NS, action=$ACTION)"
@ -263,7 +533,7 @@ echo "Deploying common services (namespace=$NS, action=$ACTION)"
rc=0 rc=0
case "$ACTION" in case "$ACTION" in
start|update|reload|initialize|restart) start|update|reload|initialize|restart|status)
migrate_common_services migrate_common_services
;; ;;
esac esac
@ -274,6 +544,13 @@ else
echo "WARN: init_opentofu.sh not found; skipping OpenTofu." echo "WARN: init_opentofu.sh not found; skipping OpenTofu."
fi fi
if [ -x "$SCRIPT_DIR/init_openbao.sh" ]; then
OPENBAO_NAMESPACE="$NS" SERVICE_NAMESPACE="$NS" \
"$SCRIPT_DIR/init_openbao.sh" -n "$NS" "$ACTION" || rc=$?
else
echo "WARN: init_openbao.sh not found; skipping OpenBao."
fi
if [ -x "$SCRIPT_DIR/init_registry.sh" ]; then if [ -x "$SCRIPT_DIR/init_registry.sh" ]; then
ARGOCD_NAMESPACE="$ARGOCD_NS" REGISTRY_NAMESPACE="$REGISTRY_NS" \ ARGOCD_NAMESPACE="$ARGOCD_NS" REGISTRY_NAMESPACE="$REGISTRY_NS" \
"$SCRIPT_DIR/init_registry.sh" -n "$ARGOCD_NS" --registry-namespace "$REGISTRY_NS" "$ACTION" || rc=$? "$SCRIPT_DIR/init_registry.sh" -n "$ARGOCD_NS" --registry-namespace "$REGISTRY_NS" "$ACTION" || rc=$?

View File

@ -0,0 +1,43 @@
#!/bin/bash
set -e
# PROLE_HOME should be set by the caller
PROLE_CONF_DIR="${PROLE_HOME:-$HOME/.prole}/conf"
mkdir -p "$PROLE_CONF_DIR"
VERSION_FILE="$PROLE_CONF_DIR/database_versions.json"
echo "Fetching latest database versions..."
# Fetch latest PG version from Docker Hub (best effort)
get_latest_pg() {
curl -s https://registry.hub.docker.com/v2/repositories/library/postgres/tags?page_size=100 | \
grep -o '"name": *"[0-9][0-9]*"' | cut -d'"' -f4 | sort -ur | head -n 1 || echo "17"
}
PG_LATEST=$(get_latest_pg)
PG_CURRENT=$((PG_LATEST - 1))
PG_STABLE=$((PG_LATEST - 2))
# For Percona, let's use similar logic or defaults
PERCONA_LATEST="$PG_LATEST"
PERCONA_CURRENT="$PG_CURRENT"
PERCONA_STABLE="$PG_STABLE"
PERCONA_18="18"
cat <<EOF > "$VERSION_FILE"
{
"postgresql": {
"stable": "$PG_STABLE",
"current": "$PG_CURRENT",
"latest": "$PG_LATEST"
},
"percona": {
"stable": "$PERCONA_STABLE",
"current": "$PERCONA_CURRENT",
"latest": "$PERCONA_LATEST",
"v18": "$PERCONA_18"
}
}
EOF
echo "Database versions updated in $VERSION_FILE"

View File

@ -10,38 +10,44 @@ set -euo pipefail
# Initialize SCRIPT_DIR # Initialize SCRIPT_DIR
SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
# Load environment and config via prole_cfg.sh # Shared option parsing for common core scripts
# shellcheck disable=SC1090
source "$SCRIPT_DIR/common_core_lib.sh"
common_core_preparse_config "$@"
# shellcheck disable=SC1090 # shellcheck disable=SC1090
source "$SCRIPT_DIR/prole_cfg.sh" source "$SCRIPT_DIR/prole_cfg.sh"
if [[ "${1:-}" == "--mode" || "${1:-}" == "-m" ]]; then set -- "${COMMON_CORE_ARGS[@]}"
prole_set_mode "${2:-}" common_core_parse_args "$@"
shift 2
elif [[ "${1:-}" == --mode=* || "${1:-}" == -m=* ]]; then if [[ "${COMMON_CORE_HELP:-0}" == 1 ]]; then
prole_set_mode "${1#*=}" common_core_usage "$0"
shift exit 0
fi fi
ACTION=${1:-} if [[ -n "${COMMON_CORE_PARSE_ERROR:-}" ]]; then
echo "ERROR: ${COMMON_CORE_PARSE_ERROR}" >&2
common_core_usage "$0"
exit 2
fi
ACTION="$COMMON_CORE_ACTION"
if [[ -n "${GARAGE_INIT_LOG:-}" ]]; then if [[ -n "${GARAGE_INIT_LOG:-}" ]]; then
mkdir -p "$(dirname "$GARAGE_INIT_LOG")" mkdir -p "$(dirname "$GARAGE_INIT_LOG")"
exec > >(tee -a "$GARAGE_INIT_LOG") 2>&1 exec > >(tee -a "$GARAGE_INIT_LOG") 2>&1
fi fi
RESOLVED_NAMESPACE="$(common_core_resolve_namespace "default")"
common_core_apply_namespace "$RESOLVED_NAMESPACE"
GARAGE_NAME=${GARAGE_NAME:-garage} GARAGE_NAME=${GARAGE_NAME:-garage}
GARAGE_SECRET_NAME=${GARAGE_SECRET_NAME:-garage-secrets} GARAGE_SECRET_NAME=${GARAGE_SECRET_NAME:-garage-secrets}
GARAGE_NODE_CAPACITY=${GARAGE_NODE_CAPACITY:-10GB} GARAGE_NODE_CAPACITY=${GARAGE_NODE_CAPACITY:-10GB}
GARAGE_ZONE=${GARAGE_ZONE:-local} GARAGE_ZONE=${GARAGE_ZONE:-local}
NAMESPACE=${NAMESPACE:-default} GARAGE_NAMESPACE=${GARAGE_NAMESPACE:-$RESOLVED_NAMESPACE}
SERVICE_NAMESPACE=${SERVICE_NAMESPACE:-}
if [[ -z "${GARAGE_NAMESPACE:-}" ]]; then
if [[ -n "$SERVICE_NAMESPACE" ]]; then
GARAGE_NAMESPACE="$SERVICE_NAMESPACE"
else
GARAGE_NAMESPACE="$NAMESPACE"
fi
fi
NAMESPACE="$GARAGE_NAMESPACE" NAMESPACE="$GARAGE_NAMESPACE"
# Support both PROLE_HOME/k8s and sibling k8s directory # Support both PROLE_HOME/k8s and sibling k8s directory
@ -72,19 +78,6 @@ GARAGE_APPLY_CHANGED=0
GARAGE_CONFIG_CHANGED=0 GARAGE_CONFIG_CHANGED=0
GARAGE_STATEFULSET_CHANGED=0 GARAGE_STATEFULSET_CHANGED=0
usage() {
cat <<USAGE
Usage: $0 [start|stop|restart|status]
Actions:
start Apply Garage manifests and initialize layout
stop Delete Garage manifests
restart Re-apply Garage manifests and initialize layout
status Show Garage resources
USAGE
exit 1
}
ensure_tools() { ensure_tools() {
for t in kubectl openssl; do for t in kubectl openssl; do
command -v "$t" >/dev/null || { echo "Missing required tool: $t" >&2; exit 1; } command -v "$t" >/dev/null || { echo "Missing required tool: $t" >&2; exit 1; }
@ -384,7 +377,7 @@ status() {
} }
case "$ACTION" in case "$ACTION" in
start) start|initialize|update|reload|restart)
ensure_tools ensure_tools
ensure_namespace ensure_namespace
ensure_secrets ensure_secrets
@ -403,24 +396,11 @@ case "$ACTION" in
echo "Deleting Garage manifests from namespace '$NAMESPACE'..." echo "Deleting Garage manifests from namespace '$NAMESPACE'..."
delete_manifests delete_manifests
;; ;;
restart)
ensure_tools
ensure_namespace
ensure_secrets
k3d_cleanup_pending_pvc
echo "Re-applying Garage manifests in namespace '$NAMESPACE'..."
apply_manifests
ensure_container_command
pin_garage_node_k3d
restart_statefulset
wait_ready
init_layout
prole_register_port_forward "garage" "${NAMESPACE:-default}" "svc/garage" "3900" "3900" "0.0.0.0" "TCP" "Garage S3 API"
;;
status) status)
status status
;; ;;
*) *)
usage common_core_usage "$0"
exit 1
;; ;;
esac esac

256
mock_val/init_gitea.sh Executable file
View File

@ -0,0 +1,256 @@
#!/usr/bin/env bash
set -euo pipefail
PROG="init_gitea"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck disable=SC1090
source "$SCRIPT_DIR/prole_cfg.sh"
MODE="$(prole_normalize_mode "${PROLE_MODE:-${DEPLOYMENT_MODE:-k3d}}")"
NAMESPACE="${GITEA_NAMESPACE:-}"
CFG_PATH=""
FORCE=0
usage() {
cat <<EOF
Usage:
$PROG [options] [deploy]
Options:
--mode <k3d|k3s|k8s|local> Deployment mode (default: ${MODE:-k3d})
-n, --namespace <name> Target namespace (default: gitea)
-c, --config <prole.cfg> Path to prole.cfg (defaults to detected)
--force Delete existing release before deploy
--help Show this help
Behavior:
- Prefers Helm install/upgrade (gitea-charts/gitea).
- Falls back to applying a generated Deployment/Service/Ingress manifest.
- If a local registry is enabled, the image is mirrored there first.
EOF
}
die() { echo "[ERROR] $*" >&2; exit 2; }
log() { echo "[INFO] $*"; }
warn() { echo "[WARN] $*" >&2; }
while [[ $# -gt 0 ]]; do
case "$1" in
--mode) MODE="$(prole_normalize_mode "${2:-}")"; shift 2 ;;
--mode=*) MODE="$(prole_normalize_mode "${1#*=}")"; shift 1 ;;
-n|--namespace) NAMESPACE="${2:-}"; shift 2 ;;
--namespace=*) NAMESPACE="${1#*=}"; shift 1 ;;
-c|--config) CFG_PATH="${2:-}"; shift 2 ;;
--config=*) CFG_PATH="${1#*=}"; shift 1 ;;
--force) FORCE=1; shift ;;
-h|--help) usage; exit 0 ;;
*) break ;;
esac
done
# Resolve config path and namespace defaults from prole.cfg when present
if [[ -z "$CFG_PATH" && -n "${PROLE_CONF:-}" && -f "${PROLE_CONF}/prole.cfg" ]]; then
CFG_PATH="${PROLE_CONF}/prole.cfg"
elif [[ -z "$CFG_PATH" && -f "$SCRIPT_DIR/../conf/prole.cfg" ]]; then
CFG_PATH="$SCRIPT_DIR/../conf/prole.cfg"
fi
if [[ -z "$NAMESPACE" && -n "$CFG_PATH" ]]; then
maybe_ns="$(_prole_cfg_extract_key "$CFG_PATH" "GITOPS_NAMESPACE")"
[[ -z "$maybe_ns" ]] && maybe_ns="$(_prole_cfg_extract_key "$CFG_PATH" "GITEA_NAMESPACE")"
NAMESPACE="$maybe_ns"
fi
NAMESPACE="${NAMESPACE:-gitea}"
export GITEA_NAMESPACE="$NAMESPACE"
case "$MODE" in
k3d|k3s|k8s|local) ;;
*) die "Unsupported mode '$MODE' (use k3d, k3s, k8s, or local)" ;;
esac
export PROLE_MODE="$MODE"
command -v kubectl >/dev/null || die "kubectl not found"
command -v helm >/dev/null || warn "helm not found — will use manifest fallback"
kubectl get ns "$NAMESPACE" >/dev/null 2>&1 || kubectl create namespace "$NAMESPACE" >/dev/null 2>&1
CHART_REPO="https://dl.gitea.com/charts"
CHART_NAME="gitea-charts/gitea"
RELEASE_NAME="gitea"
IMAGE_REPO_DEFAULT="gitea/gitea"
IMAGE_TAG="${GITEA_IMAGE_TAG:-1.22.3}"
IMAGE_REPO="$IMAGE_REPO_DEFAULT"
# Optional reset
if [[ "$FORCE" -eq 1 ]]; then
warn "--force specified; removing existing Gitea resources in namespace $NAMESPACE"
if command -v helm >/dev/null 2>&1; then
helm uninstall "$RELEASE_NAME" -n "$NAMESPACE" >/dev/null 2>&1 || true
fi
kubectl -n "$NAMESPACE" delete deploy/gitea svc/gitea-http svc/gitea-ssh ingress/gitea >/dev/null 2>&1 || true
fi
# Mirror to local registry if enabled
if _prole_local_registry_enabled && command -v docker >/dev/null; then
HOST_REG="${LOCAL_REGISTRY:-${PROLE_LOCAL_REGISTRY:-}}"
INTERNAL_REG="${LOCAL_REGISTRY_INTERNAL:-${PROLE_LOCAL_REGISTRY_INTERNAL:-}}"
if [[ -n "$HOST_REG" ]]; then
HOST_IMG="${HOST_REG}/${IMAGE_REPO_DEFAULT}"
log "Pulling ${IMAGE_REPO_DEFAULT}:${IMAGE_TAG}"
docker pull "${IMAGE_REPO_DEFAULT}:${IMAGE_TAG}" >/dev/null
log "Tagging ${HOST_IMG}:${IMAGE_TAG}"
docker tag "${IMAGE_REPO_DEFAULT}:${IMAGE_TAG}" "${HOST_IMG}:${IMAGE_TAG}"
log "Pushing ${HOST_IMG}:${IMAGE_TAG}"
docker push "${HOST_IMG}:${IMAGE_TAG}" >/dev/null || warn "Push to local registry failed; continuing with upstream image"
if [[ -n "$INTERNAL_REG" ]]; then
IMAGE_REPO="${INTERNAL_REG}/gitea/gitea"
else
IMAGE_REPO="${HOST_REG}/gitea/gitea"
fi
fi
fi
VALUES_FILE="$(mktemp /tmp/gitea-values.XXXXXX.yaml)"
cat >"$VALUES_FILE" <<EOF
image:
repository: "${IMAGE_REPO}"
tag: "${IMAGE_TAG}"
service:
http:
type: ClusterIP
port: 3000
ssh:
type: ClusterIP
port: 22
gitea:
config:
server:
DOMAIN: ${GITEA_DOMAIN:-git.prole.org}
SSH_DOMAIN: ${GITEA_SSH_DOMAIN:-git.prole.org}
ROOT_URL: "http://${GITEA_DOMAIN:-git.prole.org}:3000/"
ingress:
enabled: ${GITEA_INGRESS_ENABLED:-$( [[ "$MODE" == "k3s" || "$MODE" == "k8s" ]] && echo true || echo false )}
className: ${GITEA_INGRESS_CLASS:-nginx}
hosts:
- host: ${GITEA_DOMAIN:-git.prole.org}
paths:
- path: /
pathType: Prefix
backend:
service:
name: ${RELEASE_NAME}-http
port:
number: 3000
EOF
helm_deployed=0
if command -v helm >/dev/null; then
if ! helm repo list 2>/dev/null | grep -q "gitea-charts"; then
helm repo add gitea-charts "$CHART_REPO" >/dev/null 2>&1 || warn "helm repo add failed"
fi
helm repo update >/dev/null 2>&1 || true
log "Deploying Gitea via Helm to namespace ${NAMESPACE} (mode=${MODE})"
set +e
helm upgrade --install "$RELEASE_NAME" "$CHART_NAME" \
--namespace "$NAMESPACE" \
--create-namespace \
-f "$VALUES_FILE"
rc=$?
set -e
if [[ $rc -eq 0 ]]; then
helm_deployed=1
log "Helm deployment successful."
else
warn "Helm deployment failed (code $rc); falling back to manifests."
fi
fi
if [[ $helm_deployed -eq 0 ]]; then
MANIFEST_FILE="$(mktemp /tmp/gitea-manifest.XXXXXX.yaml)"
cat >"$MANIFEST_FILE" <<EOF
apiVersion: apps/v1
kind: Deployment
metadata:
name: gitea
spec:
replicas: 1
selector:
matchLabels:
app: gitea
template:
metadata:
labels:
app: gitea
spec:
containers:
- name: gitea
image: ${IMAGE_REPO}:${IMAGE_TAG}
ports:
- name: http
containerPort: 3000
- name: ssh
containerPort: 22
volumeMounts:
- name: data
mountPath: /data
volumes:
- name: data
emptyDir: {}
---
apiVersion: v1
kind: Service
metadata:
name: gitea-http
spec:
selector:
app: gitea
ports:
- name: http
port: 3000
targetPort: 3000
---
apiVersion: v1
kind: Service
metadata:
name: gitea-ssh
spec:
selector:
app: gitea
ports:
- name: ssh
port: 22
targetPort: 22
EOF
if [[ "$MODE" == "k3s" || "$MODE" == "k8s" ]]; then
cat >>"$MANIFEST_FILE" <<EOF
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: gitea
annotations:
nginx.ingress.kubernetes.io/proxy-body-size: "64m"
spec:
ingressClassName: nginx
rules:
- host: git.prole.org
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: gitea-http
port:
number: 3000
EOF
fi
log "Applying fallback manifest"
kubectl -n "$NAMESPACE" apply -f "$MANIFEST_FILE"
fi
# Ensure port-forward hints / conflicts handled by caller via _build_required_port_forwards
log "Gitea deployment completed in namespace '$NAMESPACE'."

View File

@ -52,7 +52,7 @@ CNPG_WAIT_TIMEOUT=${CNPG_WAIT_TIMEOUT:-300}
SERVICE_NAMESPACE=${SERVICE_NAMESPACE:-${NAMESPACE:-default}} SERVICE_NAMESPACE=${SERVICE_NAMESPACE:-${NAMESPACE:-default}}
OPENBAO_NAMESPACE=${OPENBAO_NAMESPACE:-${SERVICE_NAMESPACE:-default}} OPENBAO_NAMESPACE=${OPENBAO_NAMESPACE:-${SERVICE_NAMESPACE:-default}}
KRB5_AD_NAMESPACE=${KRB5_AD_NAMESPACE:-${SERVICE_NAMESPACE}} KRB5_AD_NAMESPACE=${KRB5_AD_NAMESPACE:-${SERVICE_NAMESPACE}}
OPENBAO_PORT_FORWARD_LOCAL=${OPENBAO_PORT_FORWARD_LOCAL:-18200} OPENBAO_PORT_FORWARD_LOCAL=${OPENBAO_PORT_FORWARD_LOCAL:-8200}
log() { printf '%s\n' "$*"; } log() { printf '%s\n' "$*"; }
err() { printf '%s\n' "$*" >&2; } err() { printf '%s\n' "$*" >&2; }
@ -237,8 +237,8 @@ openbao_url() {
if curl -sS "http://127.0.0.1:8200/v1/sys/health" >/dev/null 2>&1; then if curl -sS "http://127.0.0.1:8200/v1/sys/health" >/dev/null 2>&1; then
echo "http://127.0.0.1:8200" echo "http://127.0.0.1:8200"
return 0 return 0
elif curl -sS "http://127.0.0.1:18200/v1/sys/health" >/dev/null 2>&1; then elif curl -sS "http://127.0.0.1:8200/v1/sys/health" >/dev/null 2>&1; then
echo "http://127.0.0.1:18200" echo "http://127.0.0.1:8200"
return 0 return 0
else else
echo "" echo ""

View File

@ -287,8 +287,8 @@ openbao_url() {
if curl -sS "http://127.0.0.1:8200/v1/sys/health" >/dev/null 2>&1; then if curl -sS "http://127.0.0.1:8200/v1/sys/health" >/dev/null 2>&1; then
echo "http://127.0.0.1:8200" echo "http://127.0.0.1:8200"
return 0 return 0
elif curl -sS "http://127.0.0.1:18200/v1/sys/health" >/dev/null 2>&1; then elif curl -sS "http://127.0.0.1:8200/v1/sys/health" >/dev/null 2>&1; then
echo "http://127.0.0.1:18200" echo "http://127.0.0.1:8200"
return 0 return 0
else else
echo "" echo ""

View File

@ -1,66 +1,65 @@
#!/usr/bin/env bash #!/usr/bin/env bash
set -euo pipefail set -euo pipefail
# init_kong.sh # init_kong.sh
# Purpose: # Purpose:
# - Deploy Kong API Gateway (DB-less) into the prole-db namespace # - Deploy Kong API Gateway (DB-less) into the service namespace
# - Replaces the prole nginx deployment as the API endpoint # - Replaces the prole nginx deployment as the API endpoint
# - Routes /backup/* to prole-db-manager # - Routes /backup/* to prole-db-manager
# - Creates the kong declarative config as a ConfigMap # - Creates the kong declarative config as a ConfigMap
# - Applies the kong deployment and service manifests # - Applies the kong deployment and service manifests
# - Provides start/stop/status/restart actions # - Provides start/stop/status/restart actions
#
# Usage:
# ./init_kong.sh [--mode MODE] [-n NAMESPACE] <start|stop|status|restart>
SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
# Shared option parsing for common core scripts
# shellcheck disable=SC1090
source "$SCRIPT_DIR/common_core_lib.sh"
# Inject default config if not provided
_has_config=0
for _arg in "$@"; do
[[ "$_arg" == "-c" || "$_arg" == "--config" || "$_arg" == -c=* || "$_arg" == --config=* ]] && _has_config=1
done
if [[ $_has_config -eq 0 && -f "$SCRIPT_DIR/../conf/prole.cfg" ]]; then
set -- "-c" "$SCRIPT_DIR/../conf/prole.cfg" "$@"
fi
unset _has_config _arg
common_core_preparse_config "$@"
# shellcheck disable=SC1090 # shellcheck disable=SC1090
source "$SCRIPT_DIR/prole_cfg.sh" source "$SCRIPT_DIR/prole_cfg.sh"
if [[ "${1:-}" == "--mode" || "${1:-}" == "-m" ]]; then set -- "${COMMON_CORE_ARGS[@]}"
prole_set_mode "${2:-}" common_core_parse_args "$@"
shift 2
elif [[ "${1:-}" == --mode=* || "${1:-}" == -m=* ]]; then if [[ -z "${PROLE_MODE:-}" ]]; then
prole_set_mode "${1#*=}" export PROLE_MODE="k3s"
shift
fi fi
NAMESPACE="${NAMESPACE:-default}" if [[ "${COMMON_CORE_HELP:-0}" == 1 ]]; then
common_core_usage "$0"
exit 0
fi
while [[ $# -gt 0 ]]; do if [[ -n "${COMMON_CORE_PARSE_ERROR:-}" ]]; then
case "$1" in echo "ERROR: ${COMMON_CORE_PARSE_ERROR}" >&2
-n|--namespace) common_core_usage "$0"
shift exit 2
if [[ -z "${1:-}" ]]; then fi
echo "ERROR: -n/--namespace requires a value" >&2
exit 2 ACTION="$COMMON_CORE_ACTION"
fi NAMESPACE="$(common_core_resolve_namespace "default")"
NAMESPACE="$1" common_core_apply_namespace "$NAMESPACE"
;;
-n=*|--namespace=*)
NAMESPACE="${1#*=}"
;;
start|stop|status|restart)
ACTION="$1"
;;
-h|--help)
usage
;;
*)
echo "ERROR: Unknown argument: $1" >&2
exit 2
;;
esac
shift
done
ACTION="${ACTION:-start}"
PROLE_HOME=${PROLE_HOME:-$(cd "$SCRIPT_DIR/.." && pwd)} PROLE_HOME=${PROLE_HOME:-$(cd "$SCRIPT_DIR/.." && pwd)}
KONG_IMAGE="${KONG_IMAGE:-kong:3.9}" KONG_IMAGE="${KONG_IMAGE:-kong:3.9}"
KONG_NAME="${KONG_NAME:-prole-db-kong}" KONG_NAME="${KONG_NAME:-prole-svc-kong}"
KONG_PROXY_PORT="${KONG_PROXY_PORT:-8000}" KONG_PROXY_PORT="${KONG_PROXY_PORT:-8000}"
KONG_ADMIN_PORT="${KONG_ADMIN_PORT:-8001}" KONG_ADMIN_PORT="${KONG_ADMIN_PORT:-8001}"
KONG_CONFIG_NAME="${KONG_CONFIG_NAME:-prole-db-kong-config}" KONG_CONFIG_NAME="${KONG_CONFIG_NAME:-prole-svc-kong-config}"
# Upstream service defaults # Upstream service defaults
DB_MANAGER_SERVICE="${DB_MANAGER_SERVICE:-prole-db-manager}" DB_MANAGER_SERVICE="${DB_MANAGER_SERVICE:-prole-db-manager}"
@ -68,13 +67,14 @@ DB_MANAGER_PORT="${DB_MANAGER_PORT:-80}"
usage() { usage() {
cat <<USAGE cat <<USAGE
Usage: $0 [--mode MODE] [-n NAMESPACE] <start|stop|status|restart> Usage: $0 [--mode MODE] [-n NAMESPACE] [${COMMON_CORE_ACTIONS//|/|}] [-c conf/prole.cfg]
Actions: Actions:
start Create ConfigMap and deploy Kong to the prole-db namespace start Create ConfigMap and deploy Kong
stop Remove Kong deployment, service, and ConfigMap stop Remove Kong deployment, service, and ConfigMap
status Show Kong pod/service status status Show Kong pod/service status
restart Restart Kong pods restart Restart Kong pods
update Create or update Kong resources
USAGE USAGE
exit 1 exit 1
} }
@ -96,7 +96,7 @@ create_kong_config() {
echo "Creating/updating Kong declarative config '$KONG_CONFIG_NAME' in namespace '$NAMESPACE' ..." echo "Creating/updating Kong declarative config '$KONG_CONFIG_NAME' in namespace '$NAMESPACE' ..."
local kong_yml local kong_yml
kong_yml=$(cat <<EOF kong_yml=$(cat <<KONGEOF
_format_version: "3.0" _format_version: "3.0"
_transform: true _transform: true
@ -108,7 +108,7 @@ services:
paths: paths:
- /backup - /backup
strip_path: false strip_path: false
EOF KONGEOF
) )
kubectl create configmap "$KONG_CONFIG_NAME" \ kubectl create configmap "$KONG_CONFIG_NAME" \
@ -155,12 +155,16 @@ restart() {
echo "$KONG_NAME restarted." echo "$KONG_NAME restarted."
} }
action_update() {
ensure_tools
ensure_namespace
create_kong_config
deploy
}
case "$ACTION" in case "$ACTION" in
start) start|initialize|update|reload)
ensure_tools action_update
ensure_namespace
create_kong_config
deploy
;; ;;
stop) stop)
ensure_tools ensure_tools

View File

@ -290,8 +290,8 @@ openbao_url() {
if curl -sS "http://127.0.0.1:8200/v1/sys/health" >/dev/null 2>&1; then if curl -sS "http://127.0.0.1:8200/v1/sys/health" >/dev/null 2>&1; then
echo "http://127.0.0.1:8200" echo "http://127.0.0.1:8200"
return 0 return 0
elif curl -sS "http://127.0.0.1:18200/v1/sys/health" >/dev/null 2>&1; then elif curl -sS "http://127.0.0.1:8200/v1/sys/health" >/dev/null 2>&1; then
echo "http://127.0.0.1:18200" echo "http://127.0.0.1:8200"
return 0 return 0
else else
echo "" echo ""

View File

@ -11,22 +11,65 @@ set -euo pipefail
# Initialize SCRIPT_DIR # Initialize SCRIPT_DIR
SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
# Load environment and config via prole_cfg.sh # Shared option parsing for common core scripts
# shellcheck disable=SC1090
source "$SCRIPT_DIR/common_core_lib.sh"
# Inject default config if not provided
_has_config=0
for _arg in "$@"; do
[[ "$_arg" == "-c" || "$_arg" == "--config" || "$_arg" == -c=* || "$_arg" == --config=* ]] && _has_config=1
done
if [[ $_has_config -eq 0 && -f "$SCRIPT_DIR/../conf/prole.cfg" ]]; then
set -- "-c" "$SCRIPT_DIR/../conf/prole.cfg" "$@"
fi
unset _has_config _arg
common_core_preparse_config "$@"
# shellcheck disable=SC1090 # shellcheck disable=SC1090
source "$SCRIPT_DIR/prole_cfg.sh" source "$SCRIPT_DIR/prole_cfg.sh"
set -- "${COMMON_CORE_ARGS[@]}"
common_core_parse_args "$@"
if [[ -z "${PROLE_MODE:-}" ]]; then
export PROLE_MODE="k3s"
fi
if [[ "${COMMON_CORE_HELP:-0}" == 1 ]]; then
common_core_usage "$0"
exit 0
fi
if [[ -n "${COMMON_CORE_PARSE_ERROR:-}" ]]; then
echo "ERROR: ${COMMON_CORE_PARSE_ERROR}" >&2
common_core_usage "$0"
exit 2
fi
PRIMARY_SERVICE_NAMESPACE="${SERVICE_NAMESPACE:-default}"
ACTION="$COMMON_CORE_ACTION"
NAMESPACE="$(common_core_resolve_namespace "default")"
common_core_apply_namespace "$NAMESPACE"
if [[ -z "${PROLE_SERVICE:-}" ]]; then if [[ -z "${PROLE_SERVICE:-}" ]]; then
echo "ERROR: PROLE_SERVICE is not defined. Provide PROLE_HOME/env.sh or ~/.prole/env.sh" >&2 echo "ERROR: PROLE_SERVICE is not defined. Provide PROLE_HOME/env.sh or ~/.prole/env.sh" >&2
exit 1 exit 1
fi fi
ACTION=""
NAMESPACE_OVERRIDE=""
# Defaults # Defaults
OPENBAO_NAME=${OPENBAO_NAME:-openbao} OPENBAO_NAME=${OPENBAO_NAME:-openbao}
OPENBAO_IMAGE=${OPENBAO_IMAGE:-ghcr.io/openbao/openbao:latest} OPENBAO_IMAGE=${OPENBAO_IMAGE:-ghcr.io/openbao/openbao:latest}
OPENBAO_PORT_FORWARD_LOCAL=${OPENBAO_PORT_FORWARD_LOCAL:-18200} OPENBAO_PORT_FORWARD_LOCAL=${OPENBAO_PORT_FORWARD_LOCAL:-8200}
OPENBAO_PF_ADDR="${OPENBAO_PF_ADDR:-}"
if [[ -z "$OPENBAO_PF_ADDR" ]]; then
if [[ "${PROLE_MODE:-}" == "k3s" ]]; then
OPENBAO_PF_ADDR="0.0.0.0"
else
OPENBAO_PF_ADDR="127.0.0.1"
fi
fi
# Support both PROLE_HOME/k8s and sibling k8s directory # Support both PROLE_HOME/k8s and sibling k8s directory
if [[ -d "$SCRIPT_DIR/../k8s/openbao" ]]; then if [[ -d "$SCRIPT_DIR/../k8s/openbao" ]]; then
@ -55,55 +98,6 @@ admin_key_priv="$SECRETS_DIR/admin_ed25519.key"
admin_key_pub="$SECRETS_DIR/admin_ed25519.pub" admin_key_pub="$SECRETS_DIR/admin_ed25519.pub"
root_token_file="$SECRETS_DIR/openbao-root-token" root_token_file="$SECRETS_DIR/openbao-root-token"
while [[ $# -gt 0 ]]; do
case "$1" in
-m|--mode)
shift
prole_set_mode "${1:-}"
;;
-m=*|--mode=*)
prole_set_mode "${1#*=}"
;;
-n|--namespace)
shift
if [[ -z "${1:-}" ]]; then
echo "ERROR: -n/--namespace requires a value" >&2
exit 2
fi
NAMESPACE_OVERRIDE="$1"
;;
-n=*|--namespace=*)
NAMESPACE_OVERRIDE="${1#*=}"
;;
start|stop|status|restart|initialize|update|reload)
ACTION="$1"
;;
--)
shift
break
;;
*)
if [[ -z "$ACTION" && "$1" != -* ]]; then
ACTION="$1"
else
echo "Unknown argument: $1" >&2
exit 2
fi
;;
esac
shift
done
if [[ -n "$NAMESPACE_OVERRIDE" ]]; then
NAMESPACE="$NAMESPACE_OVERRIDE"
export NAMESPACE
fi
if [[ -z "${NAMESPACE:-}" ]]; then
NAMESPACE="default"
export NAMESPACE
fi
OPENBAO_NAMESPACE=${OPENBAO_NAMESPACE:-${SERVICE_NAMESPACE:-${NAMESPACE:-default}}} OPENBAO_NAMESPACE=${OPENBAO_NAMESPACE:-${SERVICE_NAMESPACE:-${NAMESPACE:-default}}}
OPENBAO_PATH_NAMESPACE=${OPENBAO_PATH_NAMESPACE:-${NAMESPACE:-default}} OPENBAO_PATH_NAMESPACE=${OPENBAO_PATH_NAMESPACE:-${NAMESPACE:-default}}
OPENBAO_RESOURCE_NAMESPACE="$OPENBAO_NAMESPACE" OPENBAO_RESOURCE_NAMESPACE="$OPENBAO_NAMESPACE"
@ -227,6 +221,21 @@ bao_service_url() {
echo "$PROLE_OPENBAO_URL" echo "$PROLE_OPENBAO_URL"
return 0 return 0
fi fi
if [[ "${PROLE_MODE:-}" == "k3s" ]]; then
if prole_is_in_cluster; then
echo "http://openbao.${OPENBAO_RESOURCE_NAMESPACE}.svc.cluster.local:8200"
return 0
fi
# Use k3s server host if available
local k3s_host
k3s_host=$(_prole_host_from_url "${PROLE_K3S_SERVER:-}")
if [[ -n "$k3s_host" ]]; then
echo "http://${k3s_host}:${OPENBAO_PORT_FORWARD_LOCAL:-8200}"
return 0
fi
fi
echo "http://127.0.0.1:${OPENBAO_PORT_FORWARD_LOCAL}" echo "http://127.0.0.1:${OPENBAO_PORT_FORWARD_LOCAL}"
} }
@ -234,7 +243,44 @@ bao_service_url() {
_OPENBAO_PF_PID="" _OPENBAO_PF_PID=""
_OPENBAO_PF_STARTED=0 _OPENBAO_PF_STARTED=0
should_skip_deployment() {
# If the current namespace is NOT the primary service namespace,
# we assume it's a sub-namespace (like prole-db) and we should use the common service.
if [[ "$NAMESPACE" != "$PRIMARY_SERVICE_NAMESPACE" ]]; then
return 0
fi
return 1
}
expose_common_openbao() {
local target_ns="$OPENBAO_RESOURCE_NAMESPACE"
local source_ns="$PRIMARY_SERVICE_NAMESPACE"
if [[ "$target_ns" == "$source_ns" ]]; then
return 0
fi
echo "Exposing common OpenBao service from namespace '$source_ns' to '$target_ns' ..."
# Use ExternalName to point to the common OpenBao service
cat <<EOF | kubectl apply -n "$target_ns" -f -
apiVersion: v1
kind: Service
metadata:
name: $OPENBAO_NAME
spec:
type: ExternalName
externalName: $OPENBAO_NAME.$source_ns.svc.cluster.local
EOF
}
# Use PRIMARY_SERVICE_NAMESPACE if skipping deployment (we port-forward to the common svc)
start_openbao_port_forward() { start_openbao_port_forward() {
local pf_ns="$OPENBAO_RESOURCE_NAMESPACE"
if should_skip_deployment; then
pf_ns="$PRIMARY_SERVICE_NAMESPACE"
fi
local port="${OPENBAO_PORT_FORWARD_LOCAL}" local port="${OPENBAO_PORT_FORWARD_LOCAL}"
local url="http://127.0.0.1:${port}" local url="http://127.0.0.1:${port}"
@ -242,7 +288,7 @@ start_openbao_port_forward() {
return 0 return 0
fi fi
kubectl -n "$OPENBAO_RESOURCE_NAMESPACE" port-forward "svc/${OPENBAO_NAME}" "${port}:8200" >/dev/null 2>&1 & kubectl -n "$pf_ns" port-forward --address "${OPENBAO_PF_ADDR:-127.0.0.1}" "svc/${OPENBAO_NAME}" "${port}:8200" >/dev/null 2>&1 &
_OPENBAO_PF_PID=$! _OPENBAO_PF_PID=$!
_OPENBAO_PF_STARTED=1 _OPENBAO_PF_STARTED=1
@ -268,9 +314,11 @@ stop_openbao_port_forward() {
} }
wait_for_openbao_http() { wait_for_openbao_http() {
local svc
svc="$(bao_service_url)"
local max_retries=${1:-20} local max_retries=${1:-20}
local svc="${2:-}"
if [[ -z "$svc" ]]; then
svc="$(bao_service_url)"
fi
local count=0 local count=0
while ! curl -sS "$svc/v1/sys/health" >/dev/null 2>&1; do while ! curl -sS "$svc/v1/sys/health" >/dev/null 2>&1; do
if [[ $count -ge $max_retries ]]; then if [[ $count -ge $max_retries ]]; then
@ -434,11 +482,16 @@ apply_openbao_host_network_patch() {
} }
wait_for_openbao() { wait_for_openbao() {
echo "Waiting for OpenBao to become ready ..." local wait_ns="$OPENBAO_RESOURCE_NAMESPACE"
if kubectl get statefulset/$OPENBAO_NAME -n "$OPENBAO_RESOURCE_NAMESPACE" >/dev/null 2>&1; then if should_skip_deployment; then
kubectl rollout status statefulset/$OPENBAO_NAME -n "$OPENBAO_RESOURCE_NAMESPACE" --timeout=${ROLLOUT_TIMEOUT:-300s} wait_ns="$PRIMARY_SERVICE_NAMESPACE"
fi
echo "Waiting for OpenBao to become ready in namespace '$wait_ns' ..."
if kubectl get statefulset/$OPENBAO_NAME -n "$wait_ns" >/dev/null 2>&1; then
kubectl rollout status statefulset/$OPENBAO_NAME -n "$wait_ns" --timeout=${ROLLOUT_TIMEOUT:-300s}
else else
kubectl rollout status deploy/$OPENBAO_NAME -n "$OPENBAO_RESOURCE_NAMESPACE" --timeout=${ROLLOUT_TIMEOUT:-300s} kubectl rollout status deploy/$OPENBAO_NAME -n "$wait_ns" --timeout=${ROLLOUT_TIMEOUT:-300s}
fi fi
} }
@ -446,8 +499,10 @@ init_openbao_kv_and_store_admin_key() {
local token local token
token=$(cat "$root_token_file") token=$(cat "$root_token_file")
# Enable kv (if not already) and write keys # Enable kv (if not already) and write keys
local svc local svc="${1:-}"
svc="$(bao_service_url)" if [[ -z "$svc" ]]; then
svc="$(bao_service_url)"
fi
echo "Configuring KV at $svc ..." echo "Configuring KV at $svc ..."
# Check if kv/ is already mounted # Check if kv/ is already mounted
@ -608,7 +663,7 @@ init_openbao_kv_and_store_admin_key() {
# status command implementation wrapped in a function to avoid top-level 'local' # status command implementation wrapped in a function to avoid top-level 'local'
cmd_status() { cmd_status() {
echo "--- init_primary_domain status ---" echo "--- openbao status ---"
echo "Namespace: $OPENBAO_RESOURCE_NAMESPACE" echo "Namespace: $OPENBAO_RESOURCE_NAMESPACE"
echo "Secret namespace: $BAO_NAMESPACE" echo "Secret namespace: $BAO_NAMESPACE"
echo "OpenBao name: $OPENBAO_NAME" echo "OpenBao name: $OPENBAO_NAME"
@ -679,6 +734,22 @@ cmd_status() {
# Prefer explicit override, then localhost port-forward, then cluster DNS # Prefer explicit override, then localhost port-forward, then cluster DNS
local svc_url local svc_url
svc_url="$(bao_service_url)" svc_url="$(bao_service_url)"
local pf_started=0
if ! curl -sS "$svc_url/v1/sys/health" >/dev/null 2>&1; then
if start_openbao_port_forward; then
pf_started=1
# If we started a port-forward and the original URL was not local,
# we should use the local URL for status verification.
if [[ "$svc_url" != *"127.0.0.1"* && "$svc_url" != *"localhost"* ]]; then
local local_url="http://127.0.0.1:${OPENBAO_PORT_FORWARD_LOCAL}"
if curl -sS "$local_url/v1/sys/health" >/dev/null 2>&1; then
svc_url="$local_url"
fi
fi
fi
fi
if curl -sS "$svc_url/v1/sys/health" >/dev/null 2>&1; then if curl -sS "$svc_url/v1/sys/health" >/dev/null 2>&1; then
echo "[OK] OpenBao service reachable" echo "[OK] OpenBao service reachable"
if [[ -s "$root_token_file" ]]; then if [[ -s "$root_token_file" ]]; then
@ -696,13 +767,8 @@ cmd_status() {
ok=1 ok=1
fi fi
# Service certificate existence (CNPG TLS) if [[ "$pf_started" == "1" ]]; then
local CNPG_CLUSTER_NAME stop_openbao_port_forward
CNPG_CLUSTER_NAME=${CNPG_CLUSTER_NAME:-prole-db}
if kubectl -n "$NAMESPACE" get secret "${CNPG_CLUSTER_NAME}-tls" >/dev/null 2>&1; then
echo "[OK] Service certificate secret '${CNPG_CLUSTER_NAME}-tls' exists"
else
echo "[INFO] Service certificate secret '${CNPG_CLUSTER_NAME}-tls' not found"
fi fi
echo "-------------------------------------" echo "-------------------------------------"
@ -720,16 +786,23 @@ case "$ACTION" in
ensure_tools ensure_tools
ensure_k8s_tools ensure_k8s_tools
ensure_namespace ensure_namespace
generate_openbao_manifests if should_skip_deployment; then
if should_apply_kerberos_configmap; then expose_common_openbao
generate_kerberos_configmap else
generate_openbao_manifests
if should_apply_kerberos_configmap; then
generate_kerberos_configmap
fi
k3d_cleanup_pending_openbao_pvc
apply_k8s
apply_openbao_host_network_patch
fi fi
ensure_admin_keypair
k3d_cleanup_pending_openbao_pvc
apply_k8s
apply_openbao_host_network_patch
wait_for_openbao wait_for_openbao
prole_register_port_forward "openbao" "${OPENBAO_NAMESPACE:-default}" "svc/openbao" "8200" "8200" "127.0.0.1" "TCP" "OpenBao" pf_ns="${OPENBAO_NAMESPACE:-default}"
if should_skip_deployment; then
pf_ns="$PRIMARY_SERVICE_NAMESPACE"
fi
prole_register_port_forward "openbao" "$pf_ns" "svc/openbao" "${OPENBAO_PORT_FORWARD_LOCAL}" "8200" "${OPENBAO_PF_ADDR}" "TCP" "OpenBao"
;; ;;
status) status)
ensure_tools ensure_tools
@ -759,13 +832,17 @@ case "$ACTION" in
ensure_tools ensure_tools
ensure_k8s_tools ensure_k8s_tools
ensure_namespace ensure_namespace
generate_openbao_manifests if should_skip_deployment; then
if should_apply_kerberos_configmap; then expose_common_openbao
generate_kerberos_configmap else
generate_openbao_manifests
if should_apply_kerberos_configmap; then
generate_kerberos_configmap
fi
k3d_cleanup_pending_openbao_pvc
apply_k8s
apply_openbao_host_network_patch
fi fi
k3d_cleanup_pending_openbao_pvc
apply_k8s
apply_openbao_host_network_patch
wait_for_openbao wait_for_openbao
# Read password from stdin if provided # Read password from stdin if provided
db_pass="" db_pass=""
@ -779,27 +856,43 @@ case "$ACTION" in
echo "ERROR: Unable to establish OpenBao port-forward on 127.0.0.1:${OPENBAO_PORT_FORWARD_LOCAL}" >&2 echo "ERROR: Unable to establish OpenBao port-forward on 127.0.0.1:${OPENBAO_PORT_FORWARD_LOCAL}" >&2
exit 1 exit 1
fi fi
wait_for_openbao_http 20
init_openbao_kv_and_store_admin_key # After port-forward, always use the local URL for initialization so we
prole_register_port_forward "openbao" "${OPENBAO_NAMESPACE:-default}" "svc/openbao" "8200" "8200" "127.0.0.1" "TCP" "OpenBao" # don't depend on OpenBao being externally reachable from this host.
local_url="http://127.0.0.1:${OPENBAO_PORT_FORWARD_LOCAL}"
wait_for_openbao_http 20 "$local_url"
init_openbao_kv_and_store_admin_key "$local_url"
pf_ns="${OPENBAO_NAMESPACE:-default}"
if should_skip_deployment; then
pf_ns="$PRIMARY_SERVICE_NAMESPACE"
fi
prole_register_port_forward "openbao" "$pf_ns" "svc/openbao" "${OPENBAO_PORT_FORWARD_LOCAL}" "8200" "${OPENBAO_PF_ADDR}" "TCP" "OpenBao"
echo "Initialization complete. OpenBao available at $(bao_service_url)" echo "Initialization complete. OpenBao available at $(bao_service_url)"
;; ;;
update|reload) update|reload)
ensure_tools ensure_tools
ensure_k8s_tools ensure_k8s_tools
ensure_namespace ensure_namespace
generate_openbao_manifests if should_skip_deployment; then
if should_apply_kerberos_configmap; then expose_common_openbao
generate_kerberos_configmap else
generate_openbao_manifests
if should_apply_kerberos_configmap; then
generate_kerberos_configmap
fi
k3d_cleanup_pending_openbao_pvc
apply_k8s
apply_openbao_host_network_patch
fi fi
k3d_cleanup_pending_openbao_pvc pf_ns="${OPENBAO_NAMESPACE:-default}"
apply_k8s if should_skip_deployment; then
apply_openbao_host_network_patch pf_ns="$PRIMARY_SERVICE_NAMESPACE"
prole_register_port_forward "openbao" "${OPENBAO_NAMESPACE:-default}" "svc/openbao" "8200" "8200" "127.0.0.1" "TCP" "OpenBao" fi
prole_register_port_forward "openbao" "$pf_ns" "svc/openbao" "${OPENBAO_PORT_FORWARD_LOCAL}" "8200" "${OPENBAO_PF_ADDR}" "TCP" "OpenBao"
echo "Re-applied manifests." echo "Re-applied manifests."
;; ;;
*) *)
echo "Usage: $0 [-n|--namespace NAME] {start|stop|status|restart|initialize|update|reload}" >&2 common_core_usage "$0"
exit 2 exit 2
;; ;;
esac esac

View File

@ -9,18 +9,38 @@ set -euo pipefail
SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
# Load environment and config via prole_cfg.sh # Shared option parsing for common core scripts
# shellcheck disable=SC1090
source "$SCRIPT_DIR/common_core_lib.sh"
common_core_preparse_config "$@"
# shellcheck disable=SC1090 # shellcheck disable=SC1090
source "$SCRIPT_DIR/prole_cfg.sh" source "$SCRIPT_DIR/prole_cfg.sh"
set -- "${COMMON_CORE_ARGS[@]}"
common_core_parse_args "$@"
if [[ "${COMMON_CORE_HELP:-0}" == 1 ]]; then
common_core_usage "$0"
exit 0
fi
if [[ -n "${COMMON_CORE_PARSE_ERROR:-}" ]]; then
echo "ERROR: ${COMMON_CORE_PARSE_ERROR}" >&2
common_core_usage "$0"
exit 2
fi
ACTION="$COMMON_CORE_ACTION"
NAMESPACE="$(common_core_resolve_namespace "default")"
common_core_apply_namespace "$NAMESPACE"
if [[ -z "${PROLE_SERVICE:-}" ]]; then if [[ -z "${PROLE_SERVICE:-}" ]]; then
echo "ERROR: PROLE_SERVICE is not defined. Provide PROLE_HOME/env.sh or ~/.prole/env.sh" >&2 echo "ERROR: PROLE_SERVICE is not defined. Provide PROLE_HOME/env.sh or ~/.prole/env.sh" >&2
exit 1 exit 1
fi fi
ACTION=""
NAMESPACE_OVERRIDE=""
OPENTOFU_NAME=${OPENTOFU_NAME:-opentofu} OPENTOFU_NAME=${OPENTOFU_NAME:-opentofu}
OPENTOFU_ADMIN_USER=${OPENTOFU_ADMIN_USER:-admin} OPENTOFU_ADMIN_USER=${OPENTOFU_ADMIN_USER:-admin}
@ -32,55 +52,6 @@ else
OPENTOFU_MANIFEST_DIR="$SCRIPT_DIR/../k8s/opentofu" OPENTOFU_MANIFEST_DIR="$SCRIPT_DIR/../k8s/opentofu"
fi fi
while [[ $# -gt 0 ]]; do
case "$1" in
-m|--mode)
shift
prole_set_mode "${1:-}"
;;
-m=*|--mode=*)
prole_set_mode "${1#*=}"
;;
-n|--namespace)
shift
if [[ -z "${1:-}" ]]; then
echo "ERROR: -n/--namespace requires a value" >&2
exit 2
fi
NAMESPACE_OVERRIDE="$1"
;;
-n=*|--namespace=*)
NAMESPACE_OVERRIDE="${1#*=}"
;;
start|stop|status|restart|initialize|update|reload)
ACTION="$1"
;;
--)
shift
break
;;
*)
if [[ -z "$ACTION" && "$1" != -* ]]; then
ACTION="$1"
else
echo "Unknown argument: $1" >&2
exit 2
fi
;;
esac
shift
done
if [[ -n "$NAMESPACE_OVERRIDE" ]]; then
NAMESPACE="$NAMESPACE_OVERRIDE"
export NAMESPACE
fi
if [[ -z "${NAMESPACE:-}" ]]; then
NAMESPACE="default"
export NAMESPACE
fi
OPENTOFU_NAMESPACE=${OPENTOFU_NAMESPACE:-${SERVICE_NAMESPACE:-${NAMESPACE:-default}}} OPENTOFU_NAMESPACE=${OPENTOFU_NAMESPACE:-${SERVICE_NAMESPACE:-${NAMESPACE:-default}}}
OPENTOFU_SECRET_NAMESPACE=${OPENTOFU_SECRET_NAMESPACE:-${NAMESPACE:-default}} OPENTOFU_SECRET_NAMESPACE=${OPENTOFU_SECRET_NAMESPACE:-${NAMESPACE:-default}}
OPENTOFU_OPENBAO_NAMESPACE=${OPENTOFU_OPENBAO_NAMESPACE:-${SERVICE_NAMESPACE:-${OPENTOFU_NAMESPACE}}} OPENTOFU_OPENBAO_NAMESPACE=${OPENTOFU_OPENBAO_NAMESPACE:-${SERVICE_NAMESPACE:-${OPENTOFU_NAMESPACE}}}
@ -110,8 +81,8 @@ openbao_url() {
if curl -sS "http://127.0.0.1:8200/v1/sys/health" >/dev/null 2>&1; then if curl -sS "http://127.0.0.1:8200/v1/sys/health" >/dev/null 2>&1; then
echo "http://127.0.0.1:8200" echo "http://127.0.0.1:8200"
return 0 return 0
elif curl -sS "http://127.0.0.1:18200/v1/sys/health" >/dev/null 2>&1; then elif curl -sS "http://127.0.0.1:8200/v1/sys/health" >/dev/null 2>&1; then
echo "http://127.0.0.1:18200" echo "http://127.0.0.1:8200"
return 0 return 0
else else
echo "" echo ""
@ -228,7 +199,7 @@ case "${ACTION:-}" in
status_k8s status_k8s
;; ;;
*) *)
echo "Usage: $0 [start|stop|status|restart|initialize|update|reload] [-n namespace] [--mode k3d|k3s|k8s]" >&2 common_core_usage "$0"
exit 1 exit 1
;; ;;
esac esac

View File

@ -9,6 +9,22 @@ set -euo pipefail
SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
# Shared option parsing for common core scripts
# shellcheck disable=SC1090
source "$SCRIPT_DIR/common_core_lib.sh"
# Inject default config if not provided
_has_config=0
for _arg in "$@"; do
[[ "$_arg" == "-c" || "$_arg" == "--config" || "$_arg" == -c=* || "$_arg" == --config=* ]] && _has_config=1
done
if [[ $_has_config -eq 0 && -f "$SCRIPT_DIR/../conf/prole.cfg" ]]; then
set -- "-c" "$SCRIPT_DIR/../conf/prole.cfg" "$@"
fi
unset _has_config _arg
common_core_preparse_config "$@"
# Load environment and config via prole_cfg.sh # Load environment and config via prole_cfg.sh
# shellcheck disable=SC1090 # shellcheck disable=SC1090
source "$SCRIPT_DIR/prole_cfg.sh" source "$SCRIPT_DIR/prole_cfg.sh"
@ -54,6 +70,15 @@ EOF
while [[ $# -gt 0 ]]; do while [[ $# -gt 0 ]]; do
case "$1" in case "$1" in
-c|--config)
shift
# Already handled by common_core_preparse_config if we use common_core_lib.sh
# but since we don't, we can just skip it here as prole_cfg.sh
# will pick it up if we set PROLE_CONF?
# Actually prole_cfg.sh uses PROLE_CONF.
# Let's just consume it.
shift
;;
-m|--mode) -m|--mode)
shift shift
prole_set_mode "${1:-}" prole_set_mode "${1:-}"

View File

@ -4,22 +4,30 @@ set -euo pipefail
# init_service_layer.sh # init_service_layer.sh
# Purpose: # Purpose:
# - Deploy the Prole service layer (ArgoCD, OpenTofu, Garage; Kerberos optional) # - Deploy the Prole service layer (ArgoCD, OpenTofu, Garage, OpenBao; Kerberos optional)
# - Keep service-layer resources grouped in SERVICE_NAMESPACE # - Keep service-layer resources grouped in SERVICE_NAMESPACE
# - Migrate service layer to a new namespace # - Migrate service layer to a new namespace
SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
# Shared option parsing for common core scripts
# shellcheck disable=SC1090
source "$SCRIPT_DIR/common_core_lib.sh"
# Inject default config if not provided
_has_config=0
for _arg in "$@"; do
[[ "$_arg" == "-c" || "$_arg" == "--config" || "$_arg" == -c=* || "$_arg" == --config=* ]] && _has_config=1
done
if [[ $_has_config -eq 0 && -f "$SCRIPT_DIR/../conf/prole.cfg" ]]; then
set -- "-c" "$SCRIPT_DIR/../conf/prole.cfg" "$@"
fi
unset _has_config _arg
common_core_preparse_config "$@"
# shellcheck disable=SC1090 # shellcheck disable=SC1090
source "$SCRIPT_DIR/prole_cfg.sh" source "$SCRIPT_DIR/prole_cfg.sh"
if [[ "${1:-}" == "--mode" || "${1:-}" == "-m" ]]; then
prole_set_mode "${2:-}"
shift 2
elif [[ "${1:-}" == --mode=* || "${1:-}" == -m=* ]]; then
prole_set_mode "${1#*=}"
shift
fi
prole_ensure_kubeconfig >/dev/null 2>&1 || true prole_ensure_kubeconfig >/dev/null 2>&1 || true
ACTION="" ACTION=""
@ -36,6 +44,19 @@ EOF
while [[ $# -gt 0 ]]; do while [[ $# -gt 0 ]]; do
case "$1" in case "$1" in
-c|--config)
shift
shift
;;
-m|--mode)
shift
prole_set_mode "${1:-}"
shift
;;
-m=*|--mode=*)
prole_set_mode "${1#*=}"
shift
;;
-n|--namespace) -n|--namespace)
shift shift
SERVICE_NAMESPACE_OVERRIDE="${1:-}" SERVICE_NAMESPACE_OVERRIDE="${1:-}"
@ -67,8 +88,9 @@ while [[ $# -gt 0 ]]; do
TO_NAMESPACE="${1#*=}" TO_NAMESPACE="${1#*=}"
shift shift
;; ;;
start|update|restart|status|stop|migrate|initialize|reload) start|update|restart|status|stop|migrate|initialize|reload|repair)
ACTION="$1" ACTION="$1"
[[ "$ACTION" == "repair" ]] && ACTION="update"
shift shift
;; ;;
-h|--help) -h|--help)
@ -144,7 +166,7 @@ deploy_service_layer() {
ensure_namespace "$ns" ensure_namespace "$ns"
label_namespace "$ns" label_namespace "$ns"
local argocd_action opentofu_action garage_action kdc_action local argocd_action opentofu_action garage_action kdc_action openbao_action kong_action
case "$action" in case "$action" in
start|initialize|update|reload) argocd_action="update" ;; start|initialize|update|reload) argocd_action="update" ;;
restart) argocd_action="restart" ;; restart) argocd_action="restart" ;;
@ -161,6 +183,14 @@ deploy_service_layer() {
*) opentofu_action="update" ;; *) opentofu_action="update" ;;
esac esac
case "$action" in
start|initialize|update|reload) openbao_action="update" ;;
restart) openbao_action="restart" ;;
stop) openbao_action="stop" ;;
status) openbao_action="status" ;;
*) openbao_action="update" ;;
esac
case "$action" in case "$action" in
start|initialize|update|reload) garage_action="start" ;; start|initialize|update|reload) garage_action="start" ;;
restart) garage_action="restart" ;; restart) garage_action="restart" ;;
@ -169,6 +199,14 @@ deploy_service_layer() {
*) garage_action="start" ;; *) garage_action="start" ;;
esac esac
case "$action" in
start|initialize|update|reload) kong_action="update" ;;
restart) kong_action="restart" ;;
stop) kong_action="stop" ;;
status) kong_action="status" ;;
*) kong_action="update" ;;
esac
case "$action" in case "$action" in
stop) kdc_action="cleanup" ;; stop) kdc_action="cleanup" ;;
status) kdc_action="status" ;; status) kdc_action="status" ;;
@ -181,9 +219,18 @@ deploy_service_layer() {
OPENTOFU_NAMESPACE="$ns" OPENTOFU_SECRET_NAMESPACE="${NAMESPACE:-$ns}" OPENTOFU_OPENBAO_NAMESPACE="$ns" SERVICE_NAMESPACE="$ns" \ OPENTOFU_NAMESPACE="$ns" OPENTOFU_SECRET_NAMESPACE="${NAMESPACE:-$ns}" OPENTOFU_OPENBAO_NAMESPACE="$ns" SERVICE_NAMESPACE="$ns" \
"$SCRIPT_DIR/init_opentofu.sh" -n "$ns" "$opentofu_action" || rc=$? "$SCRIPT_DIR/init_opentofu.sh" -n "$ns" "$opentofu_action" || rc=$?
OPENBAO_NAMESPACE="$ns" SERVICE_NAMESPACE="$ns" \
"$SCRIPT_DIR/init_openbao.sh" -n "$ns" "$openbao_action" || rc=$?
# cert-manager is cluster-scoped and managed independently via init_certmgr.sh
# in its own dedicated 'cert-manager' namespace; it is not part of the service layer.
NAMESPACE="$ns" SERVICE_NAMESPACE="$ns" GARAGE_NAMESPACE="$ns" \ NAMESPACE="$ns" SERVICE_NAMESPACE="$ns" GARAGE_NAMESPACE="$ns" \
"$SCRIPT_DIR/init_garage_store.sh" "$garage_action" || rc=$? "$SCRIPT_DIR/init_garage_store.sh" "$garage_action" || rc=$?
KONG_NAMESPACE="$ns" SERVICE_NAMESPACE="$ns" \
"$SCRIPT_DIR/init_kong.sh" -n "$ns" "$kong_action" || rc=$?
if [[ "$ENABLE_KERBEROS" == "1" ]]; then if [[ "$ENABLE_KERBEROS" == "1" ]]; then
SERVICE_NAMESPACE="$ns" PROLE_KDC_NAMESPACE="$ns" \ SERVICE_NAMESPACE="$ns" PROLE_KDC_NAMESPACE="$ns" \
"$SCRIPT_DIR/init_kdc.sh" "$kdc_action" || rc=$? "$SCRIPT_DIR/init_kdc.sh" "$kdc_action" || rc=$?
@ -199,8 +246,14 @@ cleanup_old_namespace() {
"$SCRIPT_DIR/init_registry.sh" -n "$ARGOCD_NS" --registry-namespace "$REGISTRY_NS" stop || true "$SCRIPT_DIR/init_registry.sh" -n "$ARGOCD_NS" --registry-namespace "$REGISTRY_NS" stop || true
OPENTOFU_NAMESPACE="$ns" SERVICE_NAMESPACE="$ns" \ OPENTOFU_NAMESPACE="$ns" SERVICE_NAMESPACE="$ns" \
"$SCRIPT_DIR/init_opentofu.sh" -n "$ns" stop || true "$SCRIPT_DIR/init_opentofu.sh" -n "$ns" stop || true
OPENBAO_NAMESPACE="$ns" SERVICE_NAMESPACE="$ns" \
"$SCRIPT_DIR/init_openbao.sh" -n "$ns" stop || true
CERTMGR_NAMESPACE="$ns" SERVICE_NAMESPACE="$ns" \
"$SCRIPT_DIR/init_certmgr.sh" -n "$ns" stop || true
NAMESPACE="$ns" SERVICE_NAMESPACE="$ns" GARAGE_NAMESPACE="$ns" \ NAMESPACE="$ns" SERVICE_NAMESPACE="$ns" GARAGE_NAMESPACE="$ns" \
"$SCRIPT_DIR/init_garage_store.sh" stop || true "$SCRIPT_DIR/init_garage_store.sh" stop || true
KONG_NAMESPACE="$ns" SERVICE_NAMESPACE="$ns" \
"$SCRIPT_DIR/init_kong.sh" -n "$ns" stop || true
if [[ "$ENABLE_KERBEROS" == "1" ]]; then if [[ "$ENABLE_KERBEROS" == "1" ]]; then
SERVICE_NAMESPACE="$ns" PROLE_KDC_NAMESPACE="$ns" \ SERVICE_NAMESPACE="$ns" PROLE_KDC_NAMESPACE="$ns" \
"$SCRIPT_DIR/init_kdc.sh" cleanup || true "$SCRIPT_DIR/init_kdc.sh" cleanup || true

View File

@ -0,0 +1,2 @@
# Registry disabled (mode=unknown) @ 2026-03-01 18:10:50

View File

@ -176,8 +176,8 @@ openbao_url() {
printf '%s' "http://127.0.0.1:8200" printf '%s' "http://127.0.0.1:8200"
return 0 return 0
fi fi
if curl -sS "http://127.0.0.1:18200/v1/sys/health" >/dev/null 2>&1; then if curl -sS "http://127.0.0.1:8200/v1/sys/health" >/dev/null 2>&1; then
printf '%s' "http://127.0.0.1:18200" printf '%s' "http://127.0.0.1:8200"
return 0 return 0
fi fi
return 1 return 1

View File

@ -463,7 +463,7 @@ def resolve_openbao_ref(value: str) -> str:
url = os.environ.get("PROLE_OPENBAO_URL") url = os.environ.get("PROLE_OPENBAO_URL")
if not url: if not url:
for p in ["8200", "18200"]: for p in ["8200", "8200"]:
try: try:
with urllib.request.urlopen(f"http://127.0.0.1:{p}/v1/sys/health", timeout=0.5) as r: with urllib.request.urlopen(f"http://127.0.0.1:{p}/v1/sys/health", timeout=0.5) as r:
if r.getcode() == 200: if r.getcode() == 200:

View File

@ -1 +1 @@
405f3fd4ceea0f00952532333e3035b240b404e9d30e0b7f root

View File

@ -2,6 +2,22 @@
set -u set -u
SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
# Shared option parsing for common core scripts
# shellcheck disable=SC1090
source "$SCRIPT_DIR/common_core_lib.sh"
# Inject default config if not provided
_has_config=0
for _arg in "$@"; do
[[ "$_arg" == "-c" || "$_arg" == "--config" || "$_arg" == -c=* || "$_arg" == --config=* ]] && _has_config=1
done
if [[ $_has_config -eq 0 && -f "$SCRIPT_DIR/../conf/prole.cfg" ]]; then
set -- "-c" "$SCRIPT_DIR/../conf/prole.cfg" "$@"
fi
unset _has_config _arg
common_core_preparse_config "$@"
# shellcheck disable=SC1090 # shellcheck disable=SC1090
source "$SCRIPT_DIR/prole_cfg.sh" source "$SCRIPT_DIR/prole_cfg.sh"
@ -9,7 +25,7 @@ usage() {
cat <<EOF cat <<EOF
Usage: status_common_services.sh [-n|--namespace NS] [-k|--kerberos] Usage: status_common_services.sh [-n|--namespace NS] [-k|--kerberos]
Checks common infrastructure services (ArgoCD, OpenTofu, Garage). Checks common infrastructure services (ArgoCD, OpenTofu, Garage, OpenBao).
Use -k to include the Prole KDC (auth) checks. Use -k to include the Prole KDC (auth) checks.
EOF EOF
} }
@ -18,6 +34,19 @@ NS=""
ENABLE_KERBEROS=0 ENABLE_KERBEROS=0
while [ $# -gt 0 ]; do while [ $# -gt 0 ]; do
case "$1" in case "$1" in
-c|--config)
shift
shift
;;
-m|--mode)
shift
prole_set_mode "${1:-}"
shift
;;
-m=*|--mode=*)
prole_set_mode "${1#*=}"
shift
;;
-n|--namespace) -n|--namespace)
shift shift
NS="${1:-}" NS="${1:-}"
@ -50,6 +79,16 @@ if [ -z "$NS" ]; then
fi fi
ARGOCD_NS="${ARGOCD_NAMESPACE:-argocd}" ARGOCD_NS="${ARGOCD_NAMESPACE:-argocd}"
KONG_NS="${KONG_NAMESPACE:-${SERVICE_NAMESPACE:-${NAMESPACE:-}}}"
CERTMGR_NS="${CERTMGR_NAMESPACE:-cert-manager}"
KONG_NAME="${KONG_NAME:-prole-svc-kong}"
if [ -z "$KONG_NS" ]; then
KONG_NS="default"
fi
if [ -z "$CERTMGR_NS" ]; then
CERTMGR_NS="cert-manager"
fi
if ! command -v kubectl >/dev/null 2>&1; then if ! command -v kubectl >/dev/null 2>&1; then
echo "ERROR: kubectl not found in PATH" echo "ERROR: kubectl not found in PATH"
@ -69,6 +108,8 @@ if [[ -n "${KUBECONFIG:-}" ]]; then
fi fi
echo "Namespace: $NS" echo "Namespace: $NS"
echo "ArgoCD Namespace: $ARGOCD_NS" echo "ArgoCD Namespace: $ARGOCD_NS"
echo "Kong Namespace: $KONG_NS"
echo "Cert-Manager Namespace: $CERTMGR_NS"
echo "" echo ""
run_cmd() { run_cmd() {
@ -79,29 +120,37 @@ run_cmd() {
echo "== Services ==" echo "== Services =="
run_cmd kubectl -n "$ARGOCD_NS" get svc argocd-server run_cmd kubectl -n "$ARGOCD_NS" get svc argocd-server
run_cmd kubectl -n "$NS" get svc opentofu garage openbao
if [[ "$ENABLE_KERBEROS" == "1" ]]; then if [[ "$ENABLE_KERBEROS" == "1" ]]; then
run_cmd kubectl -n "$NS" get svc opentofu garage auth run_cmd kubectl -n "$NS" get svc auth
else
run_cmd kubectl -n "$NS" get svc opentofu garage
fi fi
run_cmd kubectl -n "$KONG_NS" get svc "$KONG_NAME"
run_cmd kubectl -n "$CERTMGR_NS" get svc cert-manager cert-manager-webhook
echo "== Workloads ==" echo "== Workloads =="
run_cmd kubectl -n "$ARGOCD_NS" get deploy argocd-server argocd-repo-server argocd-dex-server argocd-applicationset-controller argocd-notifications-controller argocd-redis run_cmd kubectl -n "$ARGOCD_NS" get deploy argocd-server argocd-repo-server argocd-dex-server argocd-applicationset-controller argocd-notifications-controller argocd-redis
run_cmd kubectl -n "$ARGOCD_NS" get statefulset argocd-application-controller run_cmd kubectl -n "$ARGOCD_NS" get statefulset argocd-application-controller
if [[ "$ENABLE_KERBEROS" == "1" ]]; then run_cmd kubectl -n "$NS" get deploy opentofu
run_cmd kubectl -n "$NS" get deploy opentofu auth if kubectl -n "$NS" get statefulset openbao >/dev/null 2>&1; then
run_cmd kubectl -n "$NS" get statefulset openbao
else else
run_cmd kubectl -n "$NS" get deploy opentofu run_cmd kubectl -n "$NS" get deploy openbao
fi
if [[ "$ENABLE_KERBEROS" == "1" ]]; then
run_cmd kubectl -n "$NS" get deploy auth
fi fi
run_cmd kubectl -n "$NS" get statefulset garage run_cmd kubectl -n "$NS" get statefulset garage
run_cmd kubectl -n "$KONG_NS" get deploy "$KONG_NAME"
run_cmd kubectl -n "$CERTMGR_NS" get deploy cert-manager cert-manager-cainjector cert-manager-webhook
echo "== Pods ==" echo "== Pods =="
run_cmd kubectl -n "$ARGOCD_NS" get pods | grep -Ei "argocd" || true run_cmd kubectl -n "$ARGOCD_NS" get pods | grep -Ei "argocd" || true
run_cmd kubectl -n "$NS" get pods | grep -Ei "opentofu|garage|openbao" || true
if [[ "$ENABLE_KERBEROS" == "1" ]]; then if [[ "$ENABLE_KERBEROS" == "1" ]]; then
run_cmd kubectl -n "$NS" get pods | grep -Ei "opentofu|garage|auth" || true run_cmd kubectl -n "$NS" get pods | grep -Ei "auth" || true
else
run_cmd kubectl -n "$NS" get pods | grep -Ei "opentofu|garage" || true
fi fi
run_cmd kubectl -n "$KONG_NS" get pods | grep -Ei "kong" || true
run_cmd kubectl -n "$CERTMGR_NS" get pods | grep -Ei "cert-manager" || true
echo "== Health Check ==" echo "== Health Check =="
missing=0 missing=0
@ -149,6 +198,9 @@ analyze_pods() {
while IFS='|' read -r comp name ready status healthy; do while IFS='|' read -r comp name ready status healthy; do
[[ -z "$comp" ]] && continue [[ -z "$comp" ]] && continue
if [[ "$comp" == cert-manager* ]]; then
comp="certmgr"
fi
COMP_NAMESPACE["$comp"]="$ns" COMP_NAMESPACE["$comp"]="$ns"
if [[ "$healthy" == "1" ]]; then if [[ "$healthy" == "1" ]]; then
HEALTHY_COUNT["$comp"]=$(( ${HEALTHY_COUNT["$comp"]:-0} + 1 )) HEALTHY_COUNT["$comp"]=$(( ${HEALTHY_COUNT["$comp"]:-0} + 1 ))
@ -175,9 +227,13 @@ check_resource() {
check_resource svc argocd-server "$ARGOCD_NS" check_resource svc argocd-server "$ARGOCD_NS"
check_resource svc opentofu "$NS" check_resource svc opentofu "$NS"
check_resource svc garage "$NS" check_resource svc garage "$NS"
check_resource svc openbao "$NS"
if [[ "$ENABLE_KERBEROS" == "1" ]]; then if [[ "$ENABLE_KERBEROS" == "1" ]]; then
check_resource svc auth "$NS" check_resource svc auth "$NS"
fi fi
check_resource svc "$KONG_NAME" "$KONG_NS"
check_resource svc cert-manager "$CERTMGR_NS"
check_resource svc cert-manager-webhook "$CERTMGR_NS"
check_resource deploy argocd-server "$ARGOCD_NS" check_resource deploy argocd-server "$ARGOCD_NS"
check_resource deploy argocd-repo-server "$ARGOCD_NS" check_resource deploy argocd-repo-server "$ARGOCD_NS"
@ -186,18 +242,29 @@ check_resource deploy argocd-applicationset-controller "$ARGOCD_NS"
check_resource deploy argocd-notifications-controller "$ARGOCD_NS" check_resource deploy argocd-notifications-controller "$ARGOCD_NS"
check_resource deploy argocd-redis "$ARGOCD_NS" check_resource deploy argocd-redis "$ARGOCD_NS"
check_resource deploy opentofu "$NS" check_resource deploy opentofu "$NS"
if kubectl -n "$NS" get statefulset openbao >/dev/null 2>&1; then
check_resource statefulset openbao "$NS"
else
check_resource deploy openbao "$NS"
fi
if [[ "$ENABLE_KERBEROS" == "1" ]]; then if [[ "$ENABLE_KERBEROS" == "1" ]]; then
check_resource deploy auth "$NS" check_resource deploy auth "$NS"
fi fi
check_resource statefulset argocd-application-controller "$ARGOCD_NS" check_resource statefulset argocd-application-controller "$ARGOCD_NS"
check_resource statefulset garage "$NS" check_resource statefulset garage "$NS"
check_resource deploy "$KONG_NAME" "$KONG_NS"
check_resource deploy cert-manager "$CERTMGR_NS"
check_resource deploy cert-manager-cainjector "$CERTMGR_NS"
check_resource deploy cert-manager-webhook "$CERTMGR_NS"
pod_filter="opentofu|garage" pod_filter="opentofu|garage|openbao|kong|cert-manager"
if [[ "$ENABLE_KERBEROS" == "1" ]]; then if [[ "$ENABLE_KERBEROS" == "1" ]]; then
pod_filter="opentofu|garage|auth" pod_filter="opentofu|garage|openbao|auth|kong|cert-manager"
fi fi
analyze_pods "$ARGOCD_NS" "argocd" analyze_pods "$ARGOCD_NS" "argocd"
analyze_pods "$NS" "$pod_filter" analyze_pods "$NS" "$pod_filter"
analyze_pods "$KONG_NS" "kong"
analyze_pods "$CERTMGR_NS" "cert-manager"
if [[ -n "$blocked_lines" ]]; then if [[ -n "$blocked_lines" ]]; then
echo "" echo ""
@ -208,64 +275,257 @@ fi
if [ $missing -gt 0 ]; then if [ $missing -gt 0 ]; then
echo "" echo ""
echo "Status: FAILED ($missing resources missing)" # ── Attempt to repair a completely absent cert-manager before failing ──────
exit 1 if ! kubectl get namespace "$CERTMGR_NS" >/dev/null 2>&1 \
&& [[ -x "$SCRIPT_DIR/init_certmgr.sh" ]]; then
echo "== Repair: cert-manager namespace '$CERTMGR_NS' absent — re-initializing =="
"$SCRIPT_DIR/init_certmgr.sh" initialize 2>&1 || true
echo " Waiting for cert-manager to settle (20s)..."
sleep 20
echo " Re-checking cert-manager resources..."
_check_cm_resource() {
local _kind=$1 _name=$2 _ns=$3
if ! kubectl -n "$_ns" get "$_kind" "$_name" >/dev/null 2>&1; then
echo " [FAIL] $_kind/$_name still missing (ns=$_ns)"
else
missing=$((missing - 1))
echo " [OK] $_kind/$_name present (ns=$_ns)"
fi
}
_check_cm_resource svc cert-manager "$CERTMGR_NS"
_check_cm_resource svc cert-manager-webhook "$CERTMGR_NS"
_check_cm_resource deploy cert-manager "$CERTMGR_NS"
_check_cm_resource deploy cert-manager-cainjector "$CERTMGR_NS"
_check_cm_resource deploy cert-manager-webhook "$CERTMGR_NS"
fi
if [ $missing -gt 0 ]; then
echo "Status: FAILED ($missing resources missing)"
exit 1
fi
echo "Status: OK (missing resources restored by repair)"
fi fi
# ── Repair helpers ────────────────────────────────────────────────────────────
# Emit "kind name ns" lines for every workload owned by a component.
_workloads_for_comp() {
local _comp="$1"
local _ns="${COMP_NAMESPACE[$_comp]:-$NS}"
case "$_comp" in
argocd)
echo "statefulset argocd-application-controller $ARGOCD_NS"
for _d in argocd-server argocd-repo-server argocd-dex-server \
argocd-applicationset-controller argocd-notifications-controller argocd-redis; do
echo "deployment $_d $ARGOCD_NS"
done
;;
openbao)
if kubectl -n "$_ns" get statefulset openbao >/dev/null 2>&1; then
echo "statefulset openbao $_ns"
else
echo "deployment openbao $_ns"
fi
;;
opentofu) echo "deployment opentofu $_ns" ;;
garage) echo "statefulset garage $_ns" ;;
auth) echo "deployment auth $_ns" ;;
kong) echo "deployment $KONG_NAME $KONG_NS" ;;
certmgr)
for _d in cert-manager cert-manager-cainjector cert-manager-webhook; do
echo "deployment $_d $CERTMGR_NS"
done
;;
esac
}
# Reset all pod-tracking state and re-run analysis.
_recheck_pods() {
HEALTHY_COUNT=()
BLOCKED_COUNT=()
BLOCKED_PODS=()
COMP_NAMESPACE=()
blocked_lines=""
blocked=0
analyze_pods "$ARGOCD_NS" "argocd"
analyze_pods "$NS" "$pod_filter"
analyze_pods "$KONG_NS" "kong"
analyze_pods "$CERTMGR_NS" "cert-manager"
if [[ -n "$blocked_lines" ]]; then
blocked=1
fi
}
# Print the name of each component that currently has blocked pods.
_blocked_comps() {
for _bc in argocd openbao opentofu garage auth kong certmgr; do
[[ "$_bc" == "auth" && "$ENABLE_KERBEROS" != "1" ]] && continue
[[ ${BLOCKED_COUNT["$_bc"]:-0} -gt 0 ]] && echo "$_bc"
done
}
# ── Escalating repair ─────────────────────────────────────────────────────────
if [ $blocked -gt 0 ]; then if [ $blocked -gt 0 ]; then
repairable=()
fail_all=() # ── Round 1: delete blocked pods ──────────────────────────────────────────
for comp in argocd openbao opentofu garage auth; do echo ""
if [[ "$comp" == "auth" && "$ENABLE_KERBEROS" != "1" ]]; then echo "== Repair Round 1: replacing blocked pods =="
continue mapfile -t _r1_comps < <(_blocked_comps)
for _comp in "${_r1_comps[@]}"; do
_pods="${BLOCKED_PODS[$_comp]:-}"
_ns="${COMP_NAMESPACE[$_comp]:-$NS}"
if [[ -n "$_pods" ]]; then
echo " Deleting pod(s) for $_comp in $_ns:$_pods"
# shellcheck disable=SC2086
kubectl -n "$_ns" delete pod $_pods --wait=false >/dev/null 2>&1 || true
fi fi
hc=${HEALTHY_COUNT["$comp"]:-0} done
bc=${BLOCKED_COUNT["$comp"]:-0} echo " Waiting for pod replacement (15s)..."
if [[ "$bc" -gt 0 && "$hc" -gt 0 ]]; then sleep 15
repairable+=("$comp") _recheck_pods
elif [[ "$bc" -gt 0 && "$hc" -eq 0 ]]; then
fail_all+=("$comp") if [[ $blocked -eq 0 ]]; then
echo ""
echo "Status: OK (resolved in repair round 1)"
exit 0
fi
echo ""
echo "Pods still blocked after Round 1:"
printf '%b' "$blocked_lines"
# ── Round 2: rollout restart owning StatefulSet / Deployment ──────────────
echo ""
echo "== Repair Round 2: restarting workload controllers =="
mapfile -t _r2_comps < <(_blocked_comps)
for _comp in "${_r2_comps[@]}"; do
while IFS=' ' read -r _kind _name _wns; do
[[ -z "$_kind" ]] && continue
if kubectl -n "$_wns" get "$_kind" "$_name" >/dev/null 2>&1; then
echo " kubectl rollout restart $_kind/$_name -n $_wns"
kubectl -n "$_wns" rollout restart "$_kind/$_name" >/dev/null 2>&1 || true
kubectl -n "$_wns" rollout status "$_kind/$_name" --timeout=60s 2>/dev/null || true
fi
done < <(_workloads_for_comp "$_comp")
done
echo " Waiting for rollout to settle (20s)..."
sleep 20
_recheck_pods
if [[ $blocked -eq 0 ]]; then
echo ""
echo "Status: OK (resolved in repair round 2)"
exit 0
fi
echo ""
echo "Pods still blocked after Round 2:"
printf '%b' "$blocked_lines"
# ── Round 3: clear affected namespaces and re-initialize ──────────────────
echo ""
echo "== Repair Round 3: clearing affected namespaces and re-initializing =="
mapfile -t _r3_comps < <(_blocked_comps)
declare -A _cleared_ns=()
for _comp in "${_r3_comps[@]}"; do
_ns="${COMP_NAMESPACE[$_comp]:-$NS}"
[[ -n "${_cleared_ns[$_ns]:-}" ]] && continue
_cleared_ns["$_ns"]=1
if [[ "$_ns" == "default" || "$_ns" == "kube-system" ]]; then
echo " [Round 3] Removing workload resources in protected namespace '$_ns' for: $_comp"
while IFS=' ' read -r _kind _name _wns; do
[[ -z "$_kind" ]] && continue
echo " kubectl delete $_kind $_name -n $_wns --ignore-not-found"
kubectl -n "$_wns" delete "$_kind" "$_name" --ignore-not-found >/dev/null 2>&1 || true
done < <(_workloads_for_comp "$_comp")
else
echo " [Round 3] Deleting namespace '$_ns' ..."
kubectl delete namespace "$_ns" --wait=true --timeout=90s >/dev/null 2>&1 || true
echo " [Round 3] Recreating namespace '$_ns' ..."
kubectl create namespace "$_ns" >/dev/null 2>&1 || true
echo " [OK] Namespace '$_ns' cleared and recreated."
fi fi
done done
if [ ${#fail_all[@]} -gt 0 ]; then if [[ -x "$SCRIPT_DIR/init_service_layer.sh" ]]; then
echo "" echo " [Round 3] Re-initializing service layer ..."
echo "Repair skipped: all pods blocked for ${fail_all[*]}." _krb_flag=""
echo "Action: Deploy Missing Services." [[ "$ENABLE_KERBEROS" == "1" ]] && _krb_flag="-k"
echo "" # shellcheck disable=SC2086
echo "Status: NOTICE (blocked pods detected)" "$SCRIPT_DIR/init_service_layer.sh" -n "$NS" $_krb_flag update 2>&1 || true
exit 2
fi fi
if [ ${#repairable[@]} -gt 0 ]; then echo " [Round 3] Re-checking status after namespace reset (30s)..."
sleep 30
_recheck_pods
if [[ $blocked -eq 0 ]]; then
echo "" echo ""
echo "== Repair ==" echo "Status: OK (resolved in repair round 3)"
echo "Repairing blocked pods for: ${repairable[*]}" exit 0
for comp in "${repairable[@]}"; do
pods="${BLOCKED_PODS["$comp"]:-}"
ns="${COMP_NAMESPACE["$comp"]:-$NS}"
if [[ -n "$pods" ]]; then
kubectl -n "$ns" delete pod $pods --wait=false >/dev/null 2>&1 || true
fi
done
echo "Re-checking pods after repair..."
sleep 2
HEALTHY_COUNT=()
BLOCKED_COUNT=()
BLOCKED_PODS=()
COMP_NAMESPACE=()
blocked_lines=""
analyze_pods "$ARGOCD_NS" "argocd"
analyze_pods "$NS" "$pod_filter"
if [[ -n "$blocked_lines" ]]; then
echo ""
echo "Blocked pods still detected after repair:"
printf '%b' "$blocked_lines"
echo ""
echo "Status: NOTICE (blocked pods detected)"
exit 2
fi
fi fi
echo ""
echo "Pods still blocked after Round 3:"
printf '%b' "$blocked_lines"
# ── Round 4: reset k3d cluster, preserve registry ─────────────────────────
_prole_mode="${PROLE_MODE:-}"
if [[ "$_prole_mode" == "k3d" ]]; then
echo ""
echo "== Repair Round 4: resetting k3d cluster (preserving registry) =="
_cluster_name="${K3D_CLUSTER_NAME:-prole-dev-cluster}"
_registry_args=()
if command -v k3d >/dev/null 2>&1; then
_reg_name=$(k3d registry list --no-headers 2>/dev/null | awk '{print $1}' | head -1 || true)
if [[ -n "$_reg_name" ]]; then
echo " Preserving registry: $_reg_name"
_registry_args=(--registry-use "$_reg_name")
fi
fi
echo " Deleting k3d cluster '$_cluster_name' ..."
k3d cluster delete "$_cluster_name" >/dev/null 2>&1 || true
echo " Recreating k3d cluster '$_cluster_name' ..."
k3d cluster create "$_cluster_name" -a 2 \
"${_registry_args[@]}" \
--api-port 0.0.0.0:6443 >/dev/null 2>&1 || true
kubectl config use-context "k3d-${_cluster_name}" >/dev/null 2>&1 || true
if [[ -x "$SCRIPT_DIR/init_service_layer.sh" ]]; then
echo " [Round 4] Re-initializing service layer after cluster reset ..."
_krb_flag=""
[[ "$ENABLE_KERBEROS" == "1" ]] && _krb_flag="-k"
# shellcheck disable=SC2086
"$SCRIPT_DIR/init_service_layer.sh" -n "$NS" $_krb_flag update 2>&1 || true
fi
echo " [Round 4] Re-checking status after cluster reset (30s)..."
sleep 30
_recheck_pods
if [[ $blocked -eq 0 ]]; then
echo ""
echo "Status: OK (resolved in repair round 4 — cluster reset)"
exit 0
fi
echo ""
echo "Pods still blocked after Round 4 (cluster reset):"
printf '%b' "$blocked_lines"
echo ""
echo "Status: FAILED — unable to repair after full cluster reset. Manual intervention required."
exit 5
fi
echo ""
echo "Status: NOTICE (blocked pods remain; k3d cluster reset not applicable for mode '${_prole_mode:-unknown}')"
exit 3
fi fi
echo "" echo ""

View File

@ -1 +1 @@
81 89

View File

@ -80,6 +80,7 @@ RUN set -eux; \
apt-get install -y --no-install-recommends percona-postgresql-18-repack; \ apt-get install -y --no-install-recommends percona-postgresql-18-repack; \
apt-get install -y --no-install-recommends percona-pg-stat-monitor18; \ apt-get install -y --no-install-recommends percona-pg-stat-monitor18; \
apt-get install -y --no-install-recommends percona-pgbadger; \ apt-get install -y --no-install-recommends percona-pgbadger; \
apt-get install -y --no-install-recommends percona-pg-tde18; \
apt-get install -y --no-install-recommends \ apt-get install -y --no-install-recommends \
percona-postgresql-contrib \ percona-postgresql-contrib \
freetds-dev \ freetds-dev \

View File

@ -75,6 +75,7 @@ RUN set -eux; \
RUN set -eux; \ RUN set -eux; \
apt-get update; \ apt-get update; \
{{EXTENSION_INSTALL_STEPS}} {{EXTENSION_INSTALL_STEPS}}
apt-get install -y --no-install-recommends percona-pg-tde{{MAJOR_VERSION}}; \
apt-get install -y --no-install-recommends \ apt-get install -y --no-install-recommends \
percona-postgresql-contrib \ percona-postgresql-contrib \
freetds-dev \ freetds-dev \

View File

@ -1 +0,0 @@
node_modules/

View File

@ -1,19 +0,0 @@
FROM node:20-alpine
RUN apk add --no-cache curl \
&& curl -LO "https://dl.k8s.io/release/$(curl -Ls https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl" \
&& chmod +x kubectl \
&& mv kubectl /usr/local/bin/
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --omit=dev
COPY server.js ./
EXPOSE 80
USER node
CMD ["node", "server.js"]

View File

@ -1,830 +0,0 @@
{
"name": "prole-db-manager",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "prole-db-manager",
"version": "1.0.0",
"dependencies": {
"express": "^4.21.0"
},
"engines": {
"node": ">=20.0.0"
}
},
"node_modules/accepts": {
"version": "1.3.8",
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
"integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
"license": "MIT",
"dependencies": {
"mime-types": "~2.1.34",
"negotiator": "0.6.3"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/array-flatten": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
"integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==",
"license": "MIT"
},
"node_modules/body-parser": {
"version": "1.20.4",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz",
"integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==",
"license": "MIT",
"dependencies": {
"bytes": "~3.1.2",
"content-type": "~1.0.5",
"debug": "2.6.9",
"depd": "2.0.0",
"destroy": "~1.2.0",
"http-errors": "~2.0.1",
"iconv-lite": "~0.4.24",
"on-finished": "~2.4.1",
"qs": "~6.14.0",
"raw-body": "~2.5.3",
"type-is": "~1.6.18",
"unpipe": "~1.0.0"
},
"engines": {
"node": ">= 0.8",
"npm": "1.2.8000 || >= 1.4.16"
}
},
"node_modules/bytes": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
"integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/call-bind-apply-helpers": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"function-bind": "^1.1.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/call-bound": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
"integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.2",
"get-intrinsic": "^1.3.0"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/content-disposition": {
"version": "0.5.4",
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
"integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==",
"license": "MIT",
"dependencies": {
"safe-buffer": "5.2.1"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/content-type": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
"integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/cookie": {
"version": "0.7.2",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
"integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/cookie-signature": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz",
"integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==",
"license": "MIT"
},
"node_modules/debug": {
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
"license": "MIT",
"dependencies": {
"ms": "2.0.0"
}
},
"node_modules/depd": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
"integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/destroy": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
"integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==",
"license": "MIT",
"engines": {
"node": ">= 0.8",
"npm": "1.2.8000 || >= 1.4.16"
}
},
"node_modules/dunder-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.1",
"es-errors": "^1.3.0",
"gopd": "^1.2.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/ee-first": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
"integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
"license": "MIT"
},
"node_modules/encodeurl": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
"integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/es-define-property": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-errors": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-object-atoms": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/escape-html": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
"integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
"license": "MIT"
},
"node_modules/etag": {
"version": "1.8.1",
"resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
"integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/express": {
"version": "4.22.1",
"resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz",
"integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==",
"license": "MIT",
"dependencies": {
"accepts": "~1.3.8",
"array-flatten": "1.1.1",
"body-parser": "~1.20.3",
"content-disposition": "~0.5.4",
"content-type": "~1.0.4",
"cookie": "~0.7.1",
"cookie-signature": "~1.0.6",
"debug": "2.6.9",
"depd": "2.0.0",
"encodeurl": "~2.0.0",
"escape-html": "~1.0.3",
"etag": "~1.8.1",
"finalhandler": "~1.3.1",
"fresh": "~0.5.2",
"http-errors": "~2.0.0",
"merge-descriptors": "1.0.3",
"methods": "~1.1.2",
"on-finished": "~2.4.1",
"parseurl": "~1.3.3",
"path-to-regexp": "~0.1.12",
"proxy-addr": "~2.0.7",
"qs": "~6.14.0",
"range-parser": "~1.2.1",
"safe-buffer": "5.2.1",
"send": "~0.19.0",
"serve-static": "~1.16.2",
"setprototypeof": "1.2.0",
"statuses": "~2.0.1",
"type-is": "~1.6.18",
"utils-merge": "1.0.1",
"vary": "~1.1.2"
},
"engines": {
"node": ">= 0.10.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"node_modules/finalhandler": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz",
"integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==",
"license": "MIT",
"dependencies": {
"debug": "2.6.9",
"encodeurl": "~2.0.0",
"escape-html": "~1.0.3",
"on-finished": "~2.4.1",
"parseurl": "~1.3.3",
"statuses": "~2.0.2",
"unpipe": "~1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/forwarded": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
"integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/fresh": {
"version": "0.5.2",
"resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
"integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/function-bind": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/get-intrinsic": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.2",
"es-define-property": "^1.0.1",
"es-errors": "^1.3.0",
"es-object-atoms": "^1.1.1",
"function-bind": "^1.1.2",
"get-proto": "^1.0.1",
"gopd": "^1.2.0",
"has-symbols": "^1.1.0",
"hasown": "^2.0.2",
"math-intrinsics": "^1.1.0"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/get-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
"license": "MIT",
"dependencies": {
"dunder-proto": "^1.0.1",
"es-object-atoms": "^1.0.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/gopd": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-symbols": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/hasown": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
"license": "MIT",
"dependencies": {
"function-bind": "^1.1.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/http-errors": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
"integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==",
"license": "MIT",
"dependencies": {
"depd": "~2.0.0",
"inherits": "~2.0.4",
"setprototypeof": "~1.2.0",
"statuses": "~2.0.2",
"toidentifier": "~1.0.1"
},
"engines": {
"node": ">= 0.8"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"node_modules/iconv-lite": {
"version": "0.4.24",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
"integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
"license": "MIT",
"dependencies": {
"safer-buffer": ">= 2.1.2 < 3"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
"license": "ISC"
},
"node_modules/ipaddr.js": {
"version": "1.9.1",
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
"integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
"license": "MIT",
"engines": {
"node": ">= 0.10"
}
},
"node_modules/math-intrinsics": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/media-typer": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
"integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/merge-descriptors": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz",
"integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/methods": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
"integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mime": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
"integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
"license": "MIT",
"bin": {
"mime": "cli.js"
},
"engines": {
"node": ">=4"
}
},
"node_modules/mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mime-types": {
"version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"license": "MIT",
"dependencies": {
"mime-db": "1.52.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
"license": "MIT"
},
"node_modules/negotiator": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
"integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/object-inspect": {
"version": "1.13.4",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
"integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/on-finished": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
"integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
"license": "MIT",
"dependencies": {
"ee-first": "1.1.1"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/parseurl": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
"integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/path-to-regexp": {
"version": "0.1.12",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz",
"integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==",
"license": "MIT"
},
"node_modules/proxy-addr": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
"integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
"license": "MIT",
"dependencies": {
"forwarded": "0.2.0",
"ipaddr.js": "1.9.1"
},
"engines": {
"node": ">= 0.10"
}
},
"node_modules/qs": {
"version": "6.14.2",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz",
"integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==",
"license": "BSD-3-Clause",
"dependencies": {
"side-channel": "^1.1.0"
},
"engines": {
"node": ">=0.6"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/range-parser": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
"integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/raw-body": {
"version": "2.5.3",
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz",
"integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==",
"license": "MIT",
"dependencies": {
"bytes": "~3.1.2",
"http-errors": "~2.0.1",
"iconv-lite": "~0.4.24",
"unpipe": "~1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT"
},
"node_modules/safer-buffer": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
"license": "MIT"
},
"node_modules/send": {
"version": "0.19.2",
"resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz",
"integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==",
"license": "MIT",
"dependencies": {
"debug": "2.6.9",
"depd": "2.0.0",
"destroy": "1.2.0",
"encodeurl": "~2.0.0",
"escape-html": "~1.0.3",
"etag": "~1.8.1",
"fresh": "~0.5.2",
"http-errors": "~2.0.1",
"mime": "1.6.0",
"ms": "2.1.3",
"on-finished": "~2.4.1",
"range-parser": "~1.2.1",
"statuses": "~2.0.2"
},
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/send/node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"license": "MIT"
},
"node_modules/serve-static": {
"version": "1.16.3",
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz",
"integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==",
"license": "MIT",
"dependencies": {
"encodeurl": "~2.0.0",
"escape-html": "~1.0.3",
"parseurl": "~1.3.3",
"send": "~0.19.1"
},
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/setprototypeof": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
"integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
"license": "ISC"
},
"node_modules/side-channel": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
"integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"object-inspect": "^1.13.3",
"side-channel-list": "^1.0.0",
"side-channel-map": "^1.0.1",
"side-channel-weakmap": "^1.0.2"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/side-channel-list": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz",
"integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"object-inspect": "^1.13.3"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/side-channel-map": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
"integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
"license": "MIT",
"dependencies": {
"call-bound": "^1.0.2",
"es-errors": "^1.3.0",
"get-intrinsic": "^1.2.5",
"object-inspect": "^1.13.3"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/side-channel-weakmap": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
"integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
"license": "MIT",
"dependencies": {
"call-bound": "^1.0.2",
"es-errors": "^1.3.0",
"get-intrinsic": "^1.2.5",
"object-inspect": "^1.13.3",
"side-channel-map": "^1.0.1"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/statuses": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
"integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/toidentifier": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
"integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
"license": "MIT",
"engines": {
"node": ">=0.6"
}
},
"node_modules/type-is": {
"version": "1.6.18",
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
"integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
"license": "MIT",
"dependencies": {
"media-typer": "0.3.0",
"mime-types": "~2.1.24"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/unpipe": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
"integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/utils-merge": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
"integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==",
"license": "MIT",
"engines": {
"node": ">= 0.4.0"
}
},
"node_modules/vary": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
"integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
}
}
}

View File

@ -1,16 +0,0 @@
{
"name": "prole-db-manager",
"version": "1.0.0",
"description": "Prole database management REST endpoint for backup operations",
"main": "server.js",
"scripts": {
"start": "node server.js",
"test": "node --test test/"
},
"dependencies": {
"express": "^4.21.0"
},
"engines": {
"node": ">=20.0.0"
}
}

View File

@ -1,161 +0,0 @@
#!/usr/bin/env node
'use strict';
const express = require('express');
const { execFile } = require('child_process');
const { promisify } = require('util');
const execFileAsync = promisify(execFile);
const app = express();
const PORT = process.env.PORT || 80;
const NAMESPACE = process.env.NAMESPACE || 'default';
const BARMAN_PLUGIN_NAME = process.env.BARMAN_PLUGIN_NAME || 'barman-cloud.cloudnative-pg.io';
const BARMAN_OBJECT_NAME = process.env.BARMAN_OBJECT_NAME || 'prole-db-barman-objectstore';
function buildBackupManifest(clusterName, backupName) {
return JSON.stringify({
apiVersion: 'postgresql.cnpg.io/v1',
kind: 'Backup',
metadata: {
name: backupName,
namespace: NAMESPACE
},
spec: {
method: 'plugin',
pluginConfiguration: {
name: BARMAN_PLUGIN_NAME,
parameters: {
barmanObjectName: BARMAN_OBJECT_NAME
}
},
cluster: {
name: clusterName
}
}
});
}
function generateBackupName(clusterName) {
const ts = new Date().toISOString().replace(/[-:T]/g, '').replace(/\..+/, '');
return `${clusterName}-backup-${ts}`;
}
async function triggerBackup(clusterName) {
const backupName = generateBackupName(clusterName);
const manifest = buildBackupManifest(clusterName, backupName);
const { stdout, stderr } = await execFileAsync('kubectl', [
'apply', '-n', NAMESPACE, '-f', '-'
], { input: manifest, timeout: 30000 });
return { backupName, stdout: stdout.trim(), stderr: stderr.trim() };
}
async function getBackupStatus(clusterName) {
const { stdout } = await execFileAsync('kubectl', [
'get', 'backup', '-n', NAMESPACE,
'-l', `cnpg.io/cluster=${clusterName}`,
'-o', 'json'
], { timeout: 15000 });
return JSON.parse(stdout);
}
// Health check
app.get('/health', (_req, res) => {
res.json({ status: 'ok', service: 'prole-db-manager' });
});
// Trigger full backup
app.post('/backup/:clusterId/full', async (req, res) => {
const { clusterId } = req.params;
if (!clusterId || !/^[a-z0-9][a-z0-9-]*[a-z0-9]$/.test(clusterId)) {
return res.status(400).json({
error: 'Invalid cluster ID',
message: 'Cluster ID must be a valid Kubernetes resource name'
});
}
try {
const result = await triggerBackup(clusterId);
res.status(201).json({
status: 'backup_triggered',
cluster: clusterId,
backupName: result.backupName,
namespace: NAMESPACE,
message: result.stdout
});
} catch (err) {
const statusCode = err.code === 'ENOENT' ? 503 : 500;
res.status(statusCode).json({
error: 'Backup trigger failed',
cluster: clusterId,
message: err.stderr || err.message
});
}
});
// GET endpoint for convenience (e.g., curl from browser/grafana)
app.get('/backup/:clusterId/full', async (req, res) => {
const { clusterId } = req.params;
if (!clusterId || !/^[a-z0-9][a-z0-9-]*[a-z0-9]$/.test(clusterId)) {
return res.status(400).json({
error: 'Invalid cluster ID',
message: 'Cluster ID must be a valid Kubernetes resource name'
});
}
try {
const result = await triggerBackup(clusterId);
res.status(201).json({
status: 'backup_triggered',
cluster: clusterId,
backupName: result.backupName,
namespace: NAMESPACE,
message: result.stdout
});
} catch (err) {
const statusCode = err.code === 'ENOENT' ? 503 : 500;
res.status(statusCode).json({
error: 'Backup trigger failed',
cluster: clusterId,
message: err.stderr || err.message
});
}
});
// Backup status
app.get('/backup/:clusterId/status', async (req, res) => {
const { clusterId } = req.params;
try {
const backups = await getBackupStatus(clusterId);
res.json({
cluster: clusterId,
namespace: NAMESPACE,
backups: backups
});
} catch (err) {
res.status(500).json({
error: 'Failed to get backup status',
cluster: clusterId,
message: err.stderr || err.message
});
}
});
// Module exports for testing
module.exports = { app, buildBackupManifest, generateBackupName, triggerBackup, getBackupStatus };
// Start server only when run directly
if (require.main === module) {
app.listen(PORT, () => {
console.log(`prole-db-manager listening on port ${PORT}`);
console.log(` Namespace: ${NAMESPACE}`);
console.log(` Barman plugin: ${BARMAN_PLUGIN_NAME}`);
console.log(` Barman object: ${BARMAN_OBJECT_NAME}`);
});
}

View File

@ -1,143 +0,0 @@
#!/usr/bin/env node
'use strict';
const { describe, it, before, after, mock } = require('node:test');
const assert = require('node:assert/strict');
const http = require('http');
// Set test environment before requiring server
process.env.NAMESPACE = 'test-ns';
process.env.BARMAN_PLUGIN_NAME = 'barman-cloud.cloudnative-pg.io';
process.env.BARMAN_OBJECT_NAME = 'prole-db-barman-objectstore';
const { app, buildBackupManifest, generateBackupName } = require('../server');
function request(server, method, path) {
return new Promise((resolve, reject) => {
const url = new URL(path, `http://localhost:${server.address().port}`);
const req = http.request(url, { method }, (res) => {
let body = '';
res.on('data', (chunk) => { body += chunk; });
res.on('end', () => {
try {
resolve({ status: res.statusCode, body: JSON.parse(body) });
} catch {
resolve({ status: res.statusCode, body });
}
});
});
req.on('error', reject);
req.end();
});
}
describe('buildBackupManifest', () => {
it('should generate valid CNPG Backup manifest JSON', () => {
const manifest = buildBackupManifest('prole-db-a0001', 'test-backup-001');
const parsed = JSON.parse(manifest);
assert.equal(parsed.apiVersion, 'postgresql.cnpg.io/v1');
assert.equal(parsed.kind, 'Backup');
assert.equal(parsed.metadata.name, 'test-backup-001');
assert.equal(parsed.metadata.namespace, 'test-ns');
assert.equal(parsed.spec.method, 'plugin');
assert.equal(parsed.spec.pluginConfiguration.name, 'barman-cloud.cloudnative-pg.io');
assert.equal(parsed.spec.pluginConfiguration.parameters.barmanObjectName, 'prole-db-barman-objectstore');
assert.equal(parsed.spec.cluster.name, 'prole-db-a0001');
});
it('should use configured namespace from environment', () => {
const manifest = buildBackupManifest('my-cluster', 'bk-1');
const parsed = JSON.parse(manifest);
assert.equal(parsed.metadata.namespace, 'test-ns');
});
});
describe('generateBackupName', () => {
it('should include cluster name as prefix', () => {
const name = generateBackupName('prole-db-a0001');
assert.ok(name.startsWith('prole-db-a0001-backup-'));
});
it('should include a timestamp suffix', () => {
const name = generateBackupName('test-cluster');
const suffix = name.replace('test-cluster-backup-', '');
assert.ok(/^\d{14}$/.test(suffix), `Expected 14-digit timestamp, got: ${suffix}`);
});
it('should generate unique names on successive calls', () => {
const a = generateBackupName('c');
const b = generateBackupName('c');
// Within same second they may match, but structure should be valid
assert.ok(a.startsWith('c-backup-'));
assert.ok(b.startsWith('c-backup-'));
});
});
describe('HTTP endpoints', () => {
let server;
before((_, done) => {
server = app.listen(0, done);
});
after((_, done) => {
server.close(done);
});
describe('GET /health', () => {
it('should return ok status', async () => {
const res = await request(server, 'GET', '/health');
assert.equal(res.status, 200);
assert.equal(res.body.status, 'ok');
assert.equal(res.body.service, 'prole-db-manager');
});
});
describe('POST /backup/:clusterId/full', () => {
it('should reject invalid cluster IDs', async () => {
const res = await request(server, 'POST', '/backup/INVALID_NAME!/full');
assert.equal(res.status, 400);
assert.equal(res.body.error, 'Invalid cluster ID');
});
it('should reject single-char cluster ID that fails regex', async () => {
const res = await request(server, 'POST', '/backup/-/full');
assert.equal(res.status, 400);
});
it('should attempt backup for valid cluster ID (will fail without kubectl)', async () => {
const res = await request(server, 'POST', '/backup/prole-db-a0001/full');
// Without kubectl available, expect 500 or 503
assert.ok([500, 503].includes(res.status));
assert.equal(res.body.error, 'Backup trigger failed');
assert.equal(res.body.cluster, 'prole-db-a0001');
});
});
describe('GET /backup/:clusterId/full', () => {
it('should reject invalid cluster IDs', async () => {
const res = await request(server, 'GET', '/backup/INVALID!/full');
assert.equal(res.status, 400);
});
it('should attempt backup for valid cluster ID', async () => {
const res = await request(server, 'GET', '/backup/prole-db-a0001/full');
assert.ok([500, 503].includes(res.status));
assert.equal(res.body.cluster, 'prole-db-a0001');
});
});
describe('GET /backup/:clusterId/status', () => {
it('should return a response for status request', async () => {
const res = await request(server, 'GET', '/backup/prole-db-a0001/status');
assert.ok([200, 500].includes(res.status));
if (res.status === 200) {
assert.equal(res.body.cluster, 'prole-db-a0001');
assert.equal(res.body.namespace, 'test-ns');
} else {
assert.equal(res.body.error, 'Failed to get backup status');
}
});
});
});

View File

@ -0,0 +1,37 @@
"""
Shared fixtures for installer unit tests.
Resets stale tkinter MagicMocks before each test so that tests in
test_screen.py don't hit exhausted side-effect iterators left over from
test_navigation.py (which replaces sys.modules["tkinter"] at module level).
"""
import sys
import pytest
from unittest.mock import MagicMock
import installer.screen as _screen_module
@pytest.fixture(autouse=True)
def reset_tkinter_mock():
"""
Before each test, replace any stale tkinter MagicMock with a fresh
instance both in sys.modules and in installer.screen module namespace,
so TerminalConsole widget calls don't hit exhausted side-effect iterators.
"""
original_screen_tk = getattr(_screen_module, "tk", None)
if isinstance(original_screen_tk, MagicMock):
fresh = MagicMock()
_screen_module.tk = fresh
for mod_name in (
"tkinter",
"tkinter.ttk",
"tkinter.scrolledtext",
"tkinter.messagebox",
):
if mod_name in sys.modules and isinstance(sys.modules[mod_name], MagicMock):
sys.modules[mod_name] = fresh
yield
# Restore original reference so other tests in the suite are unaffected
if isinstance(original_screen_tk, MagicMock):
_screen_module.tk = original_screen_tk

View File

@ -0,0 +1,365 @@
"""
Unit tests for previously untested installer/config.py helpers.
Covers:
_get_secret_key_file, _get_file_key, normalize_version,
_extract_inline_vault_block, _try_read_ansible_vault_value,
get_dep_info (check_cmd/version_cmd paths), load_dependencies,
get_ui_icon_image_path, get_ui_background_image_path, get_properties
"""
from __future__ import annotations
import os
import subprocess
from pathlib import Path
from unittest.mock import MagicMock, patch
import pytest
from installer.config import (
_get_secret_key_file,
_get_file_key,
normalize_version,
_extract_yaml_scalar_from_text,
_extract_inline_vault_block,
_try_read_ansible_vault_value,
get_dep_info,
load_dependencies,
get_ui_icon_image_path,
get_ui_background_image_path,
get_properties,
)
# ---------------------------------------------------------------------------
# _get_secret_key_file
# ---------------------------------------------------------------------------
def test_get_secret_key_file_returns_path():
result = _get_secret_key_file()
assert isinstance(result, Path)
# ---------------------------------------------------------------------------
# _get_file_key
# ---------------------------------------------------------------------------
def test_get_file_key_generates_new_key(tmp_path):
key_path = tmp_path / "subdir" / "key.b64"
key = _get_file_key(key_path)
assert isinstance(key, bytes)
assert len(key) == 32
assert key_path.exists()
def test_get_file_key_reads_existing_key(tmp_path):
import base64
key_path = tmp_path / "key.b64"
raw_key = os.urandom(32)
key_path.write_text(base64.urlsafe_b64encode(raw_key).decode("utf-8"))
key = _get_file_key(key_path)
assert key == raw_key
def test_get_file_key_with_wrong_length_content(tmp_path):
"""If existing key file has content that decodes to non-32 bytes, still returns bytes."""
key_path = tmp_path / "key.b64"
# 'dGVzdA==' is valid base64 but decodes to only 4 bytes ('test')
key_path.write_text("dGVzdA==")
key = _get_file_key(key_path)
assert isinstance(key, bytes)
# ---------------------------------------------------------------------------
# normalize_version — edge cases
# ---------------------------------------------------------------------------
def test_normalize_version_empty():
assert normalize_version("") == ""
def test_normalize_version_simple():
assert normalize_version("1.2.3") == "01.02.03"
def test_normalize_version_no_regex_match():
"""Text with no digit sequence matching the pattern falls back to findall."""
# A string with digits but no contiguous group matching pattern
result = normalize_version("v5")
assert "05" in result
def test_normalize_version_no_digits():
"""Text with no digits at all returns stripped text."""
result = normalize_version("nodigits")
assert result == "nodigits"
def test_normalize_version_prefix():
result = normalize_version("v1.2.3-beta")
assert result == "01.02.03"
# ---------------------------------------------------------------------------
# _extract_yaml_scalar_from_text
# ---------------------------------------------------------------------------
def test_extract_yaml_scalar_simple():
text = 'key: somevalue\nother: x\n'
assert _extract_yaml_scalar_from_text(text, "key") == "somevalue"
def test_extract_yaml_scalar_missing_key():
assert _extract_yaml_scalar_from_text("a: b\n", "missing") == ""
# ---------------------------------------------------------------------------
# _extract_inline_vault_block
# ---------------------------------------------------------------------------
def test_extract_inline_vault_block_not_found():
text = "key: value\nother: x\n"
result = _extract_inline_vault_block(text, "missing")
assert result == ""
def test_extract_inline_vault_block_plain_value():
"""Key found but block doesn't start with $ANSIBLE_VAULT → return ''."""
text = "mykey:\n just some plain text\n more text\n"
result = _extract_inline_vault_block(text, "mykey")
assert result == ""
def test_extract_inline_vault_block_ansible_vault():
text = (
"mykey:\n"
" $ANSIBLE_VAULT;1.1;AES256\n"
" 6162636465666768\n"
)
result = _extract_inline_vault_block(text, "mykey")
assert "$ANSIBLE_VAULT" in result
def test_extract_inline_vault_block_no_block_lines():
"""Key found but indented block is empty → return ''."""
text = "mykey:\nother: x\n"
result = _extract_inline_vault_block(text, "mykey")
assert result == ""
# ---------------------------------------------------------------------------
# _try_read_ansible_vault_value
# ---------------------------------------------------------------------------
def test_try_read_ansible_vault_value_file_not_found(tmp_path):
result = _try_read_ansible_vault_value(tmp_path / "nonexistent.cfg", "mykey")
assert result == ""
def test_try_read_ansible_vault_value_plain_text(tmp_path):
vault = tmp_path / "prole.cfg"
vault.write_text("db_password: supersecret\n")
result = _try_read_ansible_vault_value(vault, "db_password")
assert result == "supersecret"
def test_try_read_ansible_vault_value_plain_text_vault_marker_skipped(tmp_path):
"""Plain value starting with $ANSIBLE_VAULT is treated as encrypted → skip."""
vault = tmp_path / "prole.cfg"
vault.write_text("db_password: $ANSIBLE_VAULT;1.1;AES256\n")
# No vault password file → returns ""
with patch.dict(os.environ, {"ANSIBLE_VAULT_PASSWORD_FILE": ""}, clear=False), \
patch("installer.config.shutil.which", return_value=None):
result = _try_read_ansible_vault_value(vault, "db_password")
assert result == ""
def test_try_read_ansible_vault_value_no_password_file(tmp_path):
vault = tmp_path / "prole.cfg"
vault.write_text("db_password: $ANSIBLE_VAULT;1.1;AES256\n")
with patch.dict(os.environ, {"ANSIBLE_VAULT_PASSWORD_FILE": ""}, clear=False), \
patch("installer.config.shutil.which", return_value=None):
result = _try_read_ansible_vault_value(vault, "db_password")
assert result == ""
def test_try_read_ansible_vault_value_with_vault_success(tmp_path):
vault = tmp_path / "prole.cfg"
vault.write_text("db_password: $ANSIBLE_VAULT;1.1;AES256\n")
pw_file = tmp_path / ".vault_pass"
pw_file.write_text("mypassword\n")
mock_result = MagicMock()
mock_result.returncode = 0
mock_result.stdout = "db_password: decryptedvalue\n"
with patch.dict(os.environ, {"ANSIBLE_VAULT_PASSWORD_FILE": str(pw_file)}, clear=False), \
patch("installer.config.shutil.which", return_value="/usr/bin/ansible-vault"), \
patch("installer.config.subprocess.run", return_value=mock_result):
result = _try_read_ansible_vault_value(vault, "db_password")
assert result == "decryptedvalue"
def test_try_read_ansible_vault_value_vault_fails_no_inline(tmp_path):
vault = tmp_path / "prole.cfg"
vault.write_text("db_password: $ANSIBLE_VAULT;1.1;AES256\n")
pw_file = tmp_path / ".vault_pass"
pw_file.write_text("mypassword\n")
mock_result = MagicMock()
mock_result.returncode = 1
mock_result.stdout = ""
with patch.dict(os.environ, {"ANSIBLE_VAULT_PASSWORD_FILE": str(pw_file)}, clear=False), \
patch("installer.config.shutil.which", return_value="/usr/bin/ansible-vault"), \
patch("installer.config.subprocess.run", return_value=mock_result):
result = _try_read_ansible_vault_value(vault, "db_password")
assert result == ""
def test_try_read_ansible_vault_value_uses_project_root_vault_pass(tmp_path):
"""Falls back to PROJECT_ROOT/.vault_pass if no env var set."""
vault = tmp_path / "prole.cfg"
vault.write_text("mykey: plainvalue\n")
with patch.dict(os.environ, {"ANSIBLE_VAULT_PASSWORD_FILE": ""}, clear=False):
result = _try_read_ansible_vault_value(vault, "mykey")
assert result == "plainvalue"
# ---------------------------------------------------------------------------
# get_dep_info — check_cmd and version_cmd paths
# ---------------------------------------------------------------------------
def _mock_run_results(*return_codes_and_outputs):
"""Build a side_effect list for subprocess.run."""
results = []
for rc, out in return_codes_and_outputs:
m = MagicMock()
m.returncode = rc
m.stdout = out
results.append(m)
return results
def test_get_dep_info_bin_not_found():
dep = {"id": "mytool", "name": "mytool", "bin": "mytool"}
with patch("installer.config.subprocess.run",
return_value=MagicMock(returncode=1, stdout="")):
ok, loc, ver = get_dep_info(dep)
assert ok is False
def test_get_dep_info_bin_found():
dep = {"id": "mytool", "name": "mytool", "bin": "mytool"}
with patch("installer.config.subprocess.run",
return_value=MagicMock(returncode=0, stdout="/usr/bin/mytool")):
ok, loc, ver = get_dep_info(dep)
assert ok is True
assert loc == "/usr/bin/mytool"
def test_get_dep_info_check_cmd_success():
dep = {"id": "docker", "name": "docker", "bin": "docker",
"check_cmd": "docker info"}
run_results = [
MagicMock(returncode=0, stdout="/usr/bin/docker"), # command -v
MagicMock(returncode=0, stdout="Docker version 24.0"), # check_cmd
]
with patch("installer.config.subprocess.run", side_effect=run_results):
ok, loc, ver = get_dep_info(dep)
assert ok is True
assert "Docker" in (ver or "")
def test_get_dep_info_version_cmd_success():
dep = {"id": "kubectl", "name": "kubectl", "bin": "kubectl",
"version_cmd": "kubectl version --client --short"}
run_results = [
MagicMock(returncode=0, stdout="/usr/bin/kubectl"), # command -v
MagicMock(returncode=0, stdout="Client Version: v1.28.0"), # version_cmd
]
with patch("installer.config.subprocess.run", side_effect=run_results):
ok, loc, ver = get_dep_info(dep)
assert ok is True
assert ver == "Client Version: v1.28.0"
def test_get_dep_info_exception_returns_false():
dep = {"id": "broken", "name": "broken", "bin": "broken"}
with patch("installer.config.subprocess.run", side_effect=OSError("broken")):
ok, loc, ver = get_dep_info(dep)
assert ok is False
# ---------------------------------------------------------------------------
# load_dependencies
# ---------------------------------------------------------------------------
def test_load_dependencies_no_refresh():
deps = load_dependencies(refresh=False)
assert isinstance(deps, list)
assert all(isinstance(d, dict) for d in deps)
def test_load_dependencies_with_refresh():
with patch("installer.config.get_dep_info",
return_value=(True, "/usr/bin/test", "1.0")):
deps = load_dependencies(refresh=True)
assert isinstance(deps, list)
for dep in deps:
assert dep.get("installed") is True
def test_load_dependencies_refresh_exception_handled():
"""If get_dep_info raises, dep.installed should be False."""
with patch("installer.config.get_dep_info", side_effect=RuntimeError("oops")):
deps = load_dependencies(refresh=True)
for dep in deps:
assert dep.get("installed") is False
# ---------------------------------------------------------------------------
# get_ui_icon_image_path / get_ui_background_image_path
# ---------------------------------------------------------------------------
def test_get_ui_icon_image_path_returns_path():
result = get_ui_icon_image_path()
assert isinstance(result, Path)
def test_get_ui_icon_image_path_existing_file(tmp_path):
img = tmp_path / "myicon.png"
img.write_bytes(b"PNG")
with patch("installer.config.get_config_value", return_value="myicon.png"), \
patch("installer.config.PROJECT_ROOT", tmp_path):
result = get_ui_icon_image_path()
assert result == img.resolve()
def test_get_ui_background_image_path_returns_path():
result = get_ui_background_image_path()
assert isinstance(result, Path)
def test_get_ui_background_image_path_existing_file(tmp_path):
img = tmp_path / "bg.png"
img.write_bytes(b"PNG")
with patch("installer.config.get_config_value", return_value="bg.png"), \
patch("installer.config.PROJECT_ROOT", tmp_path):
result = get_ui_background_image_path()
assert result == img.resolve()
# ---------------------------------------------------------------------------
# get_properties
# ---------------------------------------------------------------------------
def test_get_properties_returns_dict():
result = get_properties()
assert isinstance(result, dict)
def test_get_properties_consistent(tmp_path):
"""Calling get_properties twice returns consistent results."""
r1 = get_properties()
r2 = get_properties()
assert r1 == r2

View File

@ -0,0 +1,237 @@
"""
Unit tests for installer/core/controller.py
Covers ProleController: check_docker_running, get_prole_db_version, run_script.
"""
from __future__ import annotations
import os
import subprocess
from pathlib import Path
from unittest.mock import MagicMock, patch
import pytest
from installer.core.controller import ProleController
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
def _make_controller(tmp_path: Path, **kwargs) -> ProleController:
return ProleController(project_root=tmp_path, **kwargs)
def _scaffold_project(tmp_path: Path) -> None:
"""Create minimum project structure expected by run_script."""
(tmp_path / "etc").mkdir(parents=True, exist_ok=True)
(tmp_path / "conf" / "postgresql").mkdir(parents=True, exist_ok=True)
(tmp_path / "prole-db").mkdir(parents=True, exist_ok=True)
# ---------------------------------------------------------------------------
# check_docker_running
# ---------------------------------------------------------------------------
def test_check_docker_running_success(tmp_path):
ctrl = _make_controller(tmp_path)
with patch("installer.core.controller.subprocess.run") as mock_run:
mock_run.return_value = MagicMock(returncode=0)
assert ctrl.check_docker_running() is True
def test_check_docker_running_called_process_error(tmp_path):
ctrl = _make_controller(tmp_path)
with patch("installer.core.controller.subprocess.run",
side_effect=subprocess.CalledProcessError(1, "docker")):
assert ctrl.check_docker_running() is False
def test_check_docker_running_file_not_found(tmp_path):
ctrl = _make_controller(tmp_path)
with patch("installer.core.controller.subprocess.run",
side_effect=FileNotFoundError("docker not found")):
assert ctrl.check_docker_running() is False
# ---------------------------------------------------------------------------
# get_prole_db_version
# ---------------------------------------------------------------------------
def test_get_prole_db_version_no_files(tmp_path):
ctrl = _make_controller(tmp_path)
version = ctrl.get_prole_db_version()
# Falls back to defaults: 17.7 and 43 (zero-padded to 3 digits → 043)
assert version == "17.7-043"
def test_get_prole_db_version_with_files(tmp_path):
(tmp_path / "conf" / "postgresql").mkdir(parents=True)
(tmp_path / "conf" / "postgresql" / ".version").write_text("18\n")
(tmp_path / "prole-db").mkdir(parents=True)
(tmp_path / "prole-db" / ".version").write_text("88\n")
ctrl = _make_controller(tmp_path)
version = ctrl.get_prole_db_version()
assert version == "18-088"
def test_get_prole_db_version_non_numeric_release(tmp_path):
(tmp_path / "conf" / "postgresql").mkdir(parents=True)
(tmp_path / "conf" / "postgresql" / ".version").write_text("18\n")
(tmp_path / "prole-db").mkdir(parents=True)
(tmp_path / "prole-db" / ".version").write_text("latest\n")
ctrl = _make_controller(tmp_path)
version = ctrl.get_prole_db_version()
# Non-numeric release is not zero-padded
assert version == "18-latest"
def test_get_prole_db_version_only_pg_version(tmp_path):
(tmp_path / "conf" / "postgresql").mkdir(parents=True)
(tmp_path / "conf" / "postgresql" / ".version").write_text("17\n")
ctrl = _make_controller(tmp_path)
version = ctrl.get_prole_db_version()
assert version.startswith("17-")
def test_get_prole_db_version_pads_single_digit_release(tmp_path):
(tmp_path / "prole-db").mkdir(parents=True)
(tmp_path / "prole-db" / ".version").write_text("5\n")
ctrl = _make_controller(tmp_path)
version = ctrl.get_prole_db_version()
assert version.endswith("-005")
# ---------------------------------------------------------------------------
# run_script
# ---------------------------------------------------------------------------
def test_run_script_basic(tmp_path):
"""run_script executes a bash script and captures stdout via on_line."""
_scaffold_project(tmp_path)
script = tmp_path / "etc" / "hello.sh"
script.write_text("#!/bin/bash\necho 'hello from script'\n")
ctrl = _make_controller(tmp_path)
lines: list[str] = []
rc = ctrl.run_script(
"hello.sh",
env={"PROLE_HOME": str(tmp_path)},
on_line=lambda l: lines.append(l),
)
assert rc == 0
assert any("hello from script" in l for l in lines)
def test_run_script_nonzero_exit(tmp_path):
_scaffold_project(tmp_path)
script = tmp_path / "etc" / "fail.sh"
script.write_text("#!/bin/bash\nexit 42\n")
ctrl = _make_controller(tmp_path)
rc = ctrl.run_script("fail.sh", env={"PROLE_HOME": str(tmp_path)})
assert rc == 42
def test_run_script_with_args(tmp_path):
_scaffold_project(tmp_path)
script = tmp_path / "etc" / "args.sh"
script.write_text("#!/bin/bash\necho \"ARG:$1\"\n")
ctrl = _make_controller(tmp_path)
lines: list[str] = []
rc = ctrl.run_script(
"args.sh",
args=["myvalue"],
env={"PROLE_HOME": str(tmp_path)},
on_line=lambda l: lines.append(l),
)
assert rc == 0
assert any("ARG:myvalue" in l for l in lines)
def test_run_script_copies_k8s_and_conf(tmp_path):
"""Covers the k8s and conf sync paths when source dirs exist."""
_scaffold_project(tmp_path)
# Create source k8s and conf dirs
(tmp_path / "k8s").mkdir()
(tmp_path / "k8s" / "test.yaml").write_text("kind: Pod\n")
(tmp_path / "conf" / "prole.cfg").write_text("[General]\n")
script = tmp_path / "etc" / "synced.sh"
script.write_text("#!/bin/bash\necho ok\n")
target = tmp_path / "prole_home_test"
target.mkdir()
ctrl = _make_controller(tmp_path)
rc = ctrl.run_script("synced.sh", env={"PROLE_HOME": str(target)})
assert rc == 0
# k8s and conf should have been synced
assert (target / "k8s").exists()
assert (target / "conf").exists()
def test_run_script_copies_db_version(tmp_path):
"""Covers the prole-db/.version copy path."""
_scaffold_project(tmp_path)
(tmp_path / "prole-db" / ".version").write_text("88\n")
script = tmp_path / "etc" / "dbver.sh"
script.write_text("#!/bin/bash\necho ok\n")
target = tmp_path / "prole_home_db"
target.mkdir()
ctrl = _make_controller(tmp_path)
rc = ctrl.run_script("dbver.sh", env={"PROLE_HOME": str(target)})
assert rc == 0
assert (target / "prole-db" / ".version").read_text().strip() == "88"
def test_run_script_default_prole_home(tmp_path):
"""Covers the PROLE_HOME fallback to ~/.prole when not in env."""
_scaffold_project(tmp_path)
script = tmp_path / "etc" / "home.sh"
script.write_text("#!/bin/bash\necho hometest\n")
ctrl = _make_controller(tmp_path)
# Provide env without PROLE_HOME so it falls back to Path.home() / ".prole"
fake_home = tmp_path / "fakehome"
fake_home.mkdir()
with patch("installer.core.controller.Path.home", return_value=fake_home):
rc = ctrl.run_script("home.sh", env={})
assert rc == 0
def test_run_script_stderr_to_stdout_false(tmp_path):
"""Covers the stderr_to_stdout=False thread path."""
_scaffold_project(tmp_path)
script = tmp_path / "etc" / "stderr.sh"
script.write_text("#!/bin/bash\necho 'out'\n>&2 echo 'err'\n")
ctrl = _make_controller(tmp_path)
stderr_lines: list[str] = []
rc = ctrl.run_script(
"stderr.sh",
env={"PROLE_HOME": str(tmp_path)},
stderr_to_stdout=False,
on_stderr_line=lambda l: stderr_lines.append(l),
)
assert rc == 0
assert any("err" in l for l in stderr_lines)
def test_run_script_verbose_sets_env(tmp_path):
"""Covers the verbose env var injection path."""
_scaffold_project(tmp_path)
script = tmp_path / "etc" / "verbose.sh"
script.write_text("#!/bin/bash\necho \"VERBOSE:${PROLE_VERBOSE:-unset}\"\n")
ctrl = _make_controller(tmp_path, verbose=True)
lines: list[str] = []
rc = ctrl.run_script(
"verbose.sh",
env={"PROLE_HOME": str(tmp_path)},
on_line=lambda l: lines.append(l),
)
assert rc == 0
assert any("VERBOSE:1" in l for l in lines)
def test_run_script_missing_source_script(tmp_path):
"""If the source script doesn't exist, target script also won't exist — graceful."""
_scaffold_project(tmp_path)
# Don't create the script — run_script should still try to run it
ctrl = _make_controller(tmp_path)
# Should return non-zero (bash can't find the file) but not raise
rc = ctrl.run_script("nonexistent.sh", env={"PROLE_HOME": str(tmp_path)})
assert rc != 0

View File

@ -0,0 +1,132 @@
"""
Unit tests for installer/deploy.py helpers.
Covers check_xcode_tools and build_prole_app_core error paths without
requiring a real macOS Xcode build environment.
"""
from __future__ import annotations
import subprocess
from pathlib import Path
from unittest.mock import MagicMock, patch
import pytest
from installer.deploy import check_xcode_tools, build_prole_app_core
# ---------------------------------------------------------------------------
# check_xcode_tools
# ---------------------------------------------------------------------------
def test_check_xcode_tools_not_darwin():
with patch("installer.deploy.platform.system", return_value="Linux"):
assert check_xcode_tools() is False
def test_check_xcode_tools_not_darwin_windows():
with patch("installer.deploy.platform.system", return_value="Windows"):
assert check_xcode_tools() is False
def test_check_xcode_tools_darwin_success():
mock_result = MagicMock()
mock_result.returncode = 0
with patch("installer.deploy.platform.system", return_value="Darwin"), \
patch("installer.deploy.subprocess.run", return_value=mock_result):
assert check_xcode_tools() is True
def test_check_xcode_tools_darwin_failure():
mock_result = MagicMock()
mock_result.returncode = 1
with patch("installer.deploy.platform.system", return_value="Darwin"), \
patch("installer.deploy.subprocess.run", return_value=mock_result):
assert check_xcode_tools() is False
def test_check_xcode_tools_darwin_exception():
with patch("installer.deploy.platform.system", return_value="Darwin"), \
patch("installer.deploy.subprocess.run", side_effect=FileNotFoundError("xcrun not found")):
assert check_xcode_tools() is False
def test_check_xcode_tools_darwin_timeout():
with patch("installer.deploy.platform.system", return_value="Darwin"), \
patch("installer.deploy.subprocess.run", side_effect=subprocess.TimeoutExpired("xcrun", 10)):
assert check_xcode_tools() is False
# ---------------------------------------------------------------------------
# build_prole_app_core — error paths
# ---------------------------------------------------------------------------
def test_build_prole_app_core_not_darwin(tmp_path):
with patch("installer.deploy.platform.system", return_value="Linux"):
with pytest.raises(Exception, match="macOS"):
build_prole_app_core(tmp_path)
def test_build_prole_app_core_no_xcode(tmp_path):
with patch("installer.deploy.platform.system", return_value="Darwin"), \
patch("installer.deploy.check_xcode_tools", return_value=False):
with pytest.raises(Exception, match="[Xx]code"):
build_prole_app_core(tmp_path)
def test_build_prole_app_core_no_build_script(tmp_path):
# xcode ok but prole-app/build.sh missing
prole_app = tmp_path / "prole-app"
prole_app.mkdir()
with patch("installer.deploy.platform.system", return_value="Darwin"), \
patch("installer.deploy.check_xcode_tools", return_value=True):
with pytest.raises(Exception, match="[Bb]uild script"):
build_prole_app_core(tmp_path)
def test_build_prole_app_core_build_fails(tmp_path):
prole_app = tmp_path / "prole-app"
prole_app.mkdir()
build_sh = prole_app / "build.sh"
build_sh.write_text("#!/bin/bash\nexit 1\n")
mock_result = MagicMock()
mock_result.returncode = 1
mock_result.stderr = "build error"
mock_result.stdout = ""
with patch("installer.deploy.platform.system", return_value="Darwin"), \
patch("installer.deploy.check_xcode_tools", return_value=True), \
patch("installer.deploy.subprocess.run", return_value=mock_result):
with pytest.raises(Exception, match="[Bb]uild failed"):
build_prole_app_core(tmp_path)
def test_build_prole_app_core_app_missing_after_build(tmp_path):
prole_app = tmp_path / "prole-app"
prole_app.mkdir()
build_sh = prole_app / "build.sh"
build_sh.write_text("#!/bin/bash\nexit 0\n")
mock_result = MagicMock()
mock_result.returncode = 0
mock_result.stderr = ""
mock_result.stdout = "Build OK"
with patch("installer.deploy.platform.system", return_value="Darwin"), \
patch("installer.deploy.check_xcode_tools", return_value=True), \
patch("installer.deploy.subprocess.run", return_value=mock_result):
# dist/Prole.app is not created → should raise
with pytest.raises(Exception, match="[Pp]role.app"):
build_prole_app_core(tmp_path)
def test_build_prole_app_core_success(tmp_path):
prole_app = tmp_path / "prole-app"
(prole_app / "dist").mkdir(parents=True)
app_path = prole_app / "dist" / "Prole.app"
app_path.mkdir()
build_sh = prole_app / "build.sh"
build_sh.write_text("#!/bin/bash\nexit 0\n")
mock_result = MagicMock()
mock_result.returncode = 0
with patch("installer.deploy.platform.system", return_value="Darwin"), \
patch("installer.deploy.check_xcode_tools", return_value=True), \
patch("installer.deploy.subprocess.run", return_value=mock_result):
result = build_prole_app_core(tmp_path)
assert result == app_path

View File

@ -0,0 +1,649 @@
"""
Extended unit tests for installer/core/milestones.py
Covers the previously untested milestone classes:
_stream_line, DatabaseCreationMilestone, DockerBuildMilestone,
GitOpsMilestone, KerberosMilestone, SupabaseMilestone
"""
from __future__ import annotations
import sys
from pathlib import Path
from unittest.mock import MagicMock, patch, call
import pytest
from installer.core.milestones import (
_stream_line,
DatabaseCreationMilestone,
DockerBuildMilestone,
GitOpsMilestone,
KerberosMilestone,
SupabaseMilestone,
DependenciesMilestone,
InitializationScriptsMilestone,
)
from installer.state import InstallerState
from installer.core.controller import ProleController
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
def _make_state(**inputs) -> InstallerState:
state = InstallerState()
state.inputs.update(inputs)
state.controller = MagicMock(spec=ProleController)
state.controller.project_root = Path("/tmp/fake-prole")
state.controller.get_prole_db_version.return_value = "18-001"
state.controller.run_script.return_value = 0
return state
# ---------------------------------------------------------------------------
# _stream_line
# ---------------------------------------------------------------------------
def test_stream_line_writes_to_stdout(capsys):
_stream_line("hello world\n")
out = capsys.readouterr().out
assert "hello world" in out
def test_stream_line_empty_string(capsys):
_stream_line("")
# Should not raise
def test_stream_line_tolerates_broken_stdout():
with patch("sys.stdout") as mock_stdout:
mock_stdout.write.side_effect = OSError("broken pipe")
# Should not raise
_stream_line("test")
# ---------------------------------------------------------------------------
# DatabaseCreationMilestone
# ---------------------------------------------------------------------------
def test_database_creation_defaults():
state = _make_state()
milestone = DatabaseCreationMilestone()
with patch("installer.core.milestones.inst_config._encrypt_cfg_secret", return_value="enc:xxx"), \
patch("installer.core.milestones.inst_config._is_prole_secret", return_value=False):
milestone.execute(state)
assert state.config_data.get("Database Creation", {}).get("DB_NAME") == "default"
def test_database_creation_with_provided_values():
state = _make_state(**{
"init_password.db_namespace": "mydb",
"init_password.db_username": "alice",
"init_password.db_password": "secret123",
"init_password.generate_ssh_key": "False",
})
milestone = DatabaseCreationMilestone()
with patch("installer.core.milestones.inst_config._encrypt_cfg_secret", return_value="enc:xxx"), \
patch("installer.core.milestones.inst_config._is_prole_secret", return_value=False):
milestone.execute(state)
db = state.config_data.get("Database Creation", {})
assert db["DB_USER"] == "alice"
assert db["NAMESPACE"] == "mydb"
def test_database_creation_generates_password_when_missing():
state = _make_state(**{
"init_password.db_username": "bob",
"init_password.generate_ssh_key": "False",
})
milestone = DatabaseCreationMilestone()
with patch("installer.core.milestones.inst_config._encrypt_cfg_secret", return_value="enc:xxx"), \
patch("installer.core.milestones.inst_config._is_prole_secret", return_value=False):
milestone.execute(state)
# Password should have been generated and stored
assert state.inputs.get("init_password.db_password")
assert len(state.inputs["init_password.db_password"]) >= 24
def test_database_creation_resolves_secret_ref():
state = _make_state(**{
"init_password.db_username": "bob",
"init_password.db_password": "${openbao:kv/prole/db}",
"init_password.generate_ssh_key": "False",
})
milestone = DatabaseCreationMilestone()
with patch("installer.core.milestones.inst_config._resolve_secret_value", return_value="${openbao:kv/prole/db}"), \
patch("installer.core.milestones.inst_config._encrypt_cfg_secret", return_value="enc:xxx"), \
patch("installer.core.milestones.inst_config._is_prole_secret", return_value=False):
milestone.execute(state)
# Unresolvable ref → new password generated
pw = state.inputs.get("init_password.db_password", "")
assert not pw.startswith("${")
def test_database_creation_resolves_secret_ref_success():
state = _make_state(**{
"init_password.db_username": "bob",
"init_password.db_password": "${openbao:kv/prole/db}",
"init_password.generate_ssh_key": "False",
})
milestone = DatabaseCreationMilestone()
with patch("installer.core.milestones.inst_config._resolve_secret_value", return_value="realpassword"), \
patch("installer.core.milestones.inst_config._encrypt_cfg_secret", return_value="enc:xxx"), \
patch("installer.core.milestones.inst_config._is_prole_secret", return_value=False):
milestone.execute(state)
assert state.inputs["init_password.db_password"] == "realpassword"
def test_database_creation_with_progress():
state = _make_state(**{"init_password.generate_ssh_key": "False"})
milestone = DatabaseCreationMilestone()
progress = MagicMock()
with patch("installer.core.milestones.inst_config._encrypt_cfg_secret", return_value="enc:xxx"), \
patch("installer.core.milestones.inst_config._is_prole_secret", return_value=False):
milestone.execute(state, progress=progress)
progress.assert_called()
def test_database_creation_uses_env_namespace_fallback():
state = _make_state(**{
"env_setup.NAMESPACE": "staging",
"init_password.generate_ssh_key": "False",
})
milestone = DatabaseCreationMilestone()
with patch("installer.core.milestones.inst_config._encrypt_cfg_secret", return_value="enc:xxx"), \
patch("installer.core.milestones.inst_config._is_prole_secret", return_value=False):
milestone.execute(state)
assert state.config_data["Database Creation"]["NAMESPACE"] == "staging"
# ---------------------------------------------------------------------------
# DockerBuildMilestone
# ---------------------------------------------------------------------------
def test_docker_build_disabled():
state = _make_state(**{"init_db_build.run_build": "False"})
milestone = DockerBuildMilestone()
milestone.execute(state)
state.controller.run_script.assert_not_called()
def test_docker_build_runs_script():
state = _make_state(**{
"init_db_build.run_build": "True",
"init_cluster.cluster_env": "dev",
})
milestone = DockerBuildMilestone()
with patch("installer.core.env._normalize_cluster_env", return_value="dev"):
milestone.execute(state)
state.controller.run_script.assert_called_once()
args, kwargs = state.controller.run_script.call_args
assert args[0] == "build_db.sh"
def test_docker_build_non_dev_env_adds_registry():
state = _make_state(**{
"init_db_build.run_build": "True",
"init_cluster.cluster_env": "prod",
"init_cluster.k3s_server_url": "https://myrddin.prole.org:6443",
})
milestone = DockerBuildMilestone()
with patch("installer.core.env._normalize_cluster_env", return_value="prod"):
milestone.execute(state)
state.controller.run_script.assert_called_once()
_, kwargs = state.controller.run_script.call_args
build_args = kwargs.get("args", state.controller.run_script.call_args[1].get("args", []))
# Should have --push in args
assert "--push" in build_args
def test_docker_build_failed_rc():
state = _make_state(**{"init_db_build.run_build": "True"})
state.controller.run_script.return_value = 1
milestone = DockerBuildMilestone()
with patch("installer.core.env._normalize_cluster_env", return_value="dev"):
# Should not raise — just logs error
milestone.execute(state)
def test_docker_build_with_progress():
state = _make_state(**{"init_db_build.run_build": "True"})
milestone = DockerBuildMilestone()
progress = MagicMock()
with patch("installer.core.env._normalize_cluster_env", return_value="dev"):
milestone.execute(state, progress=progress)
progress.assert_called()
# ---------------------------------------------------------------------------
# GitOpsMilestone
# ---------------------------------------------------------------------------
def test_gitops_disabled():
state = _make_state(**{"init_cluster.gitops_enabled": "False"})
milestone = GitOpsMilestone()
milestone.execute(state)
assert state.config_data["GitOps"]["STATUS"] == "Skipped"
state.controller.run_script.assert_not_called()
def test_gitops_disabled_default():
state = _make_state()
milestone = GitOpsMilestone()
milestone.execute(state)
assert state.config_data["GitOps"]["STATUS"] == "Skipped"
def test_gitops_enabled_success():
state = _make_state(**{"init_cluster.gitops_enabled": "True"})
milestone = GitOpsMilestone()
with patch.object(milestone, "_get_script_env", return_value={"PROLE_MODE": "k3d"}):
milestone.execute(state)
assert state.config_data["GitOps"]["STATUS"] == "Deployed"
def test_gitops_enabled_with_progress():
state = _make_state(**{"init_cluster.gitops_enabled": "True"})
milestone = GitOpsMilestone()
progress = MagicMock()
with patch.object(milestone, "_get_script_env", return_value={"PROLE_MODE": "k3d"}):
milestone.execute(state, progress=progress)
progress.assert_called()
def test_gitops_enabled_failure_raises():
state = _make_state(**{"init_cluster.gitops_enabled": "True"})
state.controller.run_script.return_value = 1
milestone = GitOpsMilestone()
with patch.object(milestone, "_get_script_env", return_value={"PROLE_MODE": "k3d"}):
with pytest.raises(Exception, match="[Gg]itea"):
milestone.execute(state)
assert state.config_data["GitOps"]["STATUS"] == "Attempted"
def test_gitops_uses_cfg_path_when_exists(tmp_path):
state = _make_state(**{"init_cluster.gitops_enabled": "True"})
state.controller.project_root = tmp_path
(tmp_path / "conf").mkdir()
(tmp_path / "conf" / "prole.cfg").write_text("[General]\n")
milestone = GitOpsMilestone()
with patch.object(milestone, "_get_script_env", return_value={"PROLE_MODE": ""}):
milestone.execute(state)
_, kwargs = state.controller.run_script.call_args
assert "-c" in kwargs.get("args", [])
# ---------------------------------------------------------------------------
# KerberosMilestone
# ---------------------------------------------------------------------------
def test_kerberos_disabled():
state = _make_state(**{"kerberos_config.enabled": "False"})
milestone = KerberosMilestone()
milestone.execute(state)
assert state.config_data["Kerberos Authentication"]["STATUS"] == "Skipped"
state.controller.run_script.assert_not_called()
def test_kerberos_disabled_default():
state = _make_state()
milestone = KerberosMilestone()
milestone.execute(state)
assert state.config_data["Kerberos Authentication"]["STATUS"] == "Skipped"
def test_kerberos_disabled_with_progress():
state = _make_state(**{"kerberos_config.enabled": "False"})
milestone = KerberosMilestone()
progress = MagicMock()
milestone.execute(state, progress=progress)
progress.assert_called_with("Kerberos disabled; skipping", 1.0)
def test_kerberos_enabled_success():
state = _make_state(**{"kerberos_config.enabled": "True"})
milestone = KerberosMilestone()
with patch.object(milestone, "_get_script_env", return_value={"PROLE_MODE": "k3d"}):
milestone.execute(state)
assert state.config_data["Kerberos Authentication"]["STATUS"] == "Initialized"
state.controller.run_script.assert_called_once()
_, kwargs = state.controller.run_script.call_args
assert "initialize" in kwargs.get("args", [])
def test_kerberos_enabled_with_progress():
state = _make_state(**{"kerberos_config.enabled": "True"})
milestone = KerberosMilestone()
progress = MagicMock()
with patch.object(milestone, "_get_script_env", return_value={"PROLE_MODE": ""}):
milestone.execute(state, progress=progress)
progress.assert_called()
def test_kerberos_enabled_failure_raises():
state = _make_state(**{"kerberos_config.enabled": "True"})
state.controller.run_script.return_value = 1
milestone = KerberosMilestone()
with patch.object(milestone, "_get_script_env", return_value={"PROLE_MODE": ""}):
with pytest.raises(Exception, match="[Kk]erberos"):
milestone.execute(state)
assert state.config_data["Kerberos Authentication"]["STATUS"] == "Attempted"
def test_kerberos_enabled_mode_in_args():
state = _make_state(**{"kerberos_config.enabled": "True"})
milestone = KerberosMilestone()
with patch.object(milestone, "_get_script_env", return_value={"PROLE_MODE": "k3d"}):
milestone.execute(state)
_, kwargs = state.controller.run_script.call_args
assert "--mode" in kwargs.get("args", [])
assert "k3d" in kwargs.get("args", [])
def test_kerberos_enabled_no_mode():
state = _make_state(**{"kerberos_config.enabled": "True"})
milestone = KerberosMilestone()
with patch.object(milestone, "_get_script_env", return_value={"PROLE_MODE": ""}):
milestone.execute(state)
_, kwargs = state.controller.run_script.call_args
assert "--mode" not in kwargs.get("args", [])
# ---------------------------------------------------------------------------
# SupabaseMilestone
# ---------------------------------------------------------------------------
def test_supabase_disabled():
state = _make_state(**{"init_cluster.supabase_enabled": "False"})
milestone = SupabaseMilestone()
milestone.execute(state)
assert state.config_data["Supabase"]["STATUS"] == "Skipped"
def test_supabase_disabled_default():
state = _make_state()
milestone = SupabaseMilestone()
milestone.execute(state)
assert state.config_data["Supabase"]["STATUS"] == "Skipped"
def test_supabase_disabled_with_progress():
state = _make_state(**{"init_cluster.supabase_enabled": "False"})
milestone = SupabaseMilestone()
progress = MagicMock()
milestone.execute(state, progress=progress)
progress.assert_called_with("Supabase disabled; skipping", 1.0)
def test_supabase_enabled_script_missing(tmp_path):
state = _make_state(**{"init_cluster.supabase_enabled": "True"})
state.controller.project_root = tmp_path
milestone = SupabaseMilestone()
with patch.object(milestone, "_get_script_env", return_value={}):
milestone.execute(state)
# deploy.sh doesn't exist → status Attempted, no exception
assert state.config_data["Supabase"]["STATUS"] == "Attempted"
def test_supabase_enabled_success(tmp_path):
state = _make_state(**{
"init_cluster.supabase_enabled": "True",
"init_cluster.cluster_env": "dev",
})
state.controller.project_root = tmp_path
supa_dir = tmp_path / "supabase"
supa_dir.mkdir()
(supa_dir / "deploy.sh").write_text("#!/bin/bash\nexit 0\n")
mock_result = MagicMock()
mock_result.returncode = 0
milestone = SupabaseMilestone()
with patch.object(milestone, "_get_script_env", return_value={}), \
patch("installer.core.milestones.subprocess.run", return_value=mock_result), \
patch("installer.core.env._normalize_cluster_env", return_value="dev"), \
patch("installer.core.env._parse_bool", return_value=False):
milestone.execute(state)
assert state.config_data["Supabase"]["STATUS"] == "Deployed"
def test_supabase_enabled_failure(tmp_path):
state = _make_state(**{
"init_cluster.supabase_enabled": "True",
"init_cluster.cluster_env": "dev",
})
state.controller.project_root = tmp_path
supa_dir = tmp_path / "supabase"
supa_dir.mkdir()
(supa_dir / "deploy.sh").write_text("#!/bin/bash\nexit 1\n")
mock_result = MagicMock()
mock_result.returncode = 1
milestone = SupabaseMilestone()
with patch.object(milestone, "_get_script_env", return_value={}), \
patch("installer.core.milestones.subprocess.run", return_value=mock_result), \
patch("installer.core.env._normalize_cluster_env", return_value="dev"), \
patch("installer.core.env._parse_bool", return_value=False):
with pytest.raises(Exception, match="[Ss]upabase"):
milestone.execute(state)
assert state.config_data["Supabase"]["STATUS"] == "Attempted"
def test_supabase_enabled_with_progress(tmp_path):
state = _make_state(**{
"init_cluster.supabase_enabled": "True",
"init_cluster.cluster_env": "dev",
})
state.controller.project_root = tmp_path
supa_dir = tmp_path / "supabase"
supa_dir.mkdir()
(supa_dir / "deploy.sh").write_text("#!/bin/bash\nexit 0\n")
mock_result = MagicMock()
mock_result.returncode = 0
milestone = SupabaseMilestone()
progress = MagicMock()
with patch.object(milestone, "_get_script_env", return_value={}), \
patch("installer.core.milestones.subprocess.run", return_value=mock_result), \
patch("installer.core.env._normalize_cluster_env", return_value="dev"), \
patch("installer.core.env._parse_bool", return_value=False):
milestone.execute(state, progress=progress)
progress.assert_called()
# ---------------------------------------------------------------------------
# DependenciesMilestone — error paths not covered by existing tests
# ---------------------------------------------------------------------------
def test_dependencies_auto_install_disabled():
"""When auto_install=False, set status Missing and return without installing."""
state = _make_state(**{"dependencies.auto_install_missing": "False"})
milestone = DependenciesMilestone()
missing_dep = {"id": "brew", "name": "Homebrew", "install_cmd": "brew-install"}
with patch("installer.config.get_dep_info", return_value=(False, None, None)):
milestone.execute(state)
assert state.config_data.get("Dependencies", {}).get("STATUS") == "Missing"
def test_dependencies_no_install_cmd():
"""A missing dep with no install_cmd should log error and be skipped."""
state = _make_state(**{"dependencies.auto_install_missing": "True"})
milestone = DependenciesMilestone()
# First call: dep missing; second call (re-verify): still missing
with patch("installer.config.get_dep_info", return_value=(False, None, None)), \
patch("installer.config.DEPENDENCIES",
[{"id": "mytool", "name": "MyTool"}]):
milestone.execute(state)
assert state.config_data.get("Dependencies", {}).get("STATUS") == "Missing"
def test_dependencies_install_skipped_by_config():
"""Dep with install disabled in state.inputs should be skipped."""
state = _make_state(**{
"dependencies.auto_install_missing": "True",
"dependencies.brew.install": "False",
})
milestone = DependenciesMilestone()
dep = {"id": "brew", "name": "Homebrew", "install_cmd": "brew-install"}
with patch("installer.config.get_dep_info", return_value=(False, None, None)), \
patch("installer.config.DEPENDENCIES", [dep]):
milestone.execute(state)
assert state.config_data.get("Dependencies", {}).get("STATUS") == "Missing"
def test_dependencies_install_fails():
"""When the install command returns non-zero, still mark Missing."""
state = _make_state(**{"dependencies.auto_install_missing": "True"})
milestone = DependenciesMilestone()
dep = {"id": "brew", "name": "Homebrew", "install_cmd": "brew-install"}
with patch("installer.config.get_dep_info", return_value=(False, None, None)), \
patch("installer.config.DEPENDENCIES", [dep]), \
patch.object(milestone, "_run_cmd", return_value=1):
milestone.execute(state)
assert state.config_data.get("Dependencies", {}).get("STATUS") == "Missing"
def test_dependencies_install_succeeds_all():
"""After install, re-verify passes → status 'All installed'."""
state = _make_state(**{"dependencies.auto_install_missing": "True"})
milestone = DependenciesMilestone()
dep = {"id": "brew", "name": "Homebrew", "install_cmd": "brew-install"}
call_count = {"n": 0}
def _dep_info(d):
call_count["n"] += 1
# First call (initial check): missing. Re-verify calls: installed.
if call_count["n"] <= len([dep]):
return (False, None, None)
return (True, "/usr/bin/brew", "4.0")
with patch("installer.config.get_dep_info", side_effect=_dep_info), \
patch("installer.config.DEPENDENCIES", [dep]), \
patch.object(milestone, "_run_cmd", return_value=0):
milestone.execute(state)
assert state.config_data.get("Dependencies", {}).get("STATUS") == "All installed"
# ---------------------------------------------------------------------------
# InitializationScriptsMilestone._regenerate_port_mapping_cfg
# ---------------------------------------------------------------------------
def test_regenerate_port_mapping_cfg_k3d_mode(tmp_path):
state = _make_state()
state.config_data["Port Forwards"] = {
"PORT_FORWARD_K3D_MAPPING_argocd": (
"id=argocd;namespace=argocd;target=svc/argocd-server;"
"hostPort=8080;servicePort=80;address=127.0.0.1"
),
}
milestone = InitializationScriptsMilestone()
with patch("installer.core.milestones.inst_config.PROJECT_ROOT", tmp_path):
(tmp_path / "conf").mkdir(exist_ok=True)
milestone._regenerate_port_mapping_cfg(state, "k3d")
mapping = (tmp_path / "conf" / "port-mapping.cfg").read_text()
assert "argocd: local=8080" in mapping
def test_regenerate_port_mapping_cfg_k3s_mode(tmp_path):
state = _make_state()
state.config_data["Port Forwards"] = {
"PORT_FORWARD_K3S_MAPPING_openbao": (
"id=openbao;namespace=default;target=svc/openbao;"
"hostPort=8200;servicePort=8200;address=127.0.0.1"
),
}
milestone = InitializationScriptsMilestone()
with patch("installer.core.milestones.inst_config.PROJECT_ROOT", tmp_path):
(tmp_path / "conf").mkdir(exist_ok=True)
milestone._regenerate_port_mapping_cfg(state, "k3s")
mapping = (tmp_path / "conf" / "port-mapping.cfg").read_text()
assert "openbao: local=8200" in mapping
def test_regenerate_port_mapping_cfg_skips_wrong_prefix(tmp_path):
state = _make_state()
state.config_data["Port Forwards"] = {
"PORT_FORWARD_K3S_MAPPING_x": (
"id=x;namespace=default;target=svc/x;hostPort=9000;servicePort=9000;address=127.0.0.1"
),
}
milestone = InitializationScriptsMilestone()
with patch("installer.core.milestones.inst_config.PROJECT_ROOT", tmp_path):
(tmp_path / "conf").mkdir(exist_ok=True)
# k3d mode should skip k3s-prefixed entries
milestone._regenerate_port_mapping_cfg(state, "k3d")
mapping = (tmp_path / "conf" / "port-mapping.cfg").read_text()
assert "x: local=" not in mapping
def test_regenerate_port_mapping_cfg_resolves_namespace(tmp_path):
state = _make_state(**{"init_password.db_namespace": "myns"})
state.config_data["Port Forwards"] = {
"PORT_FORWARD_K3D_MAPPING_svc": (
"id=svc;namespace=${NAMESPACE};target=svc/mysvc;"
"hostPort=5000;servicePort=5000;address=127.0.0.1"
),
}
milestone = InitializationScriptsMilestone()
with patch("installer.core.milestones.inst_config.PROJECT_ROOT", tmp_path):
(tmp_path / "conf").mkdir(exist_ok=True)
milestone._regenerate_port_mapping_cfg(state, "k3d")
mapping = (tmp_path / "conf" / "port-mapping.cfg").read_text()
assert "ns=myns" in mapping
def test_regenerate_port_mapping_cfg_skips_unresolvable_port(tmp_path):
state = _make_state()
state.config_data["Port Forwards"] = {
"PORT_FORWARD_K3D_MAPPING_bad": (
"id=bad;namespace=default;target=svc/bad;"
"hostPort=${UNRESOLVED_PORT};servicePort=9999;address=127.0.0.1"
),
}
milestone = InitializationScriptsMilestone()
with patch("installer.core.milestones.inst_config.PROJECT_ROOT", tmp_path):
(tmp_path / "conf").mkdir(exist_ok=True)
milestone._regenerate_port_mapping_cfg(state, "k3d")
mapping = (tmp_path / "conf" / "port-mapping.cfg").read_text()
assert "bad: local=" not in mapping
def test_regenerate_port_mapping_cfg_empty_section(tmp_path):
state = _make_state()
milestone = InitializationScriptsMilestone()
with patch("installer.core.milestones.inst_config.PROJECT_ROOT", tmp_path):
(tmp_path / "conf").mkdir(exist_ok=True)
milestone._regenerate_port_mapping_cfg(state, "k3d")
mapping = (tmp_path / "conf" / "port-mapping.cfg").read_text()
assert "# Port mappings" in mapping
def test_regenerate_port_mapping_cfg_write_error(tmp_path):
"""Write failure should be caught and logged, not raised."""
state = _make_state()
milestone = InitializationScriptsMilestone()
with patch("installer.core.milestones.inst_config.PROJECT_ROOT", tmp_path), \
patch("pathlib.Path.write_text", side_effect=OSError("disk full")):
(tmp_path / "conf").mkdir(exist_ok=True)
# Should not raise
milestone._regenerate_port_mapping_cfg(state, "k3d")
def test_supabase_k8s_mode_for_prod(tmp_path):
state = _make_state(**{
"init_cluster.supabase_enabled": "True",
"init_cluster.cluster_env": "prod",
})
state.controller.project_root = tmp_path
supa_dir = tmp_path / "supabase"
supa_dir.mkdir()
(supa_dir / "deploy.sh").write_text("#!/bin/bash\nexit 0\n")
mock_result = MagicMock()
mock_result.returncode = 0
captured_cmd = []
def capture_run(cmd, **kwargs):
captured_cmd.extend(cmd)
return mock_result
milestone = SupabaseMilestone()
with patch.object(milestone, "_get_script_env", return_value={}), \
patch("installer.core.milestones.subprocess.run", side_effect=capture_run), \
patch("installer.core.env._normalize_cluster_env", return_value="prod"), \
patch("installer.core.env._parse_bool", return_value=False):
milestone.execute(state)
assert "k8s" in captured_cmd

View File

@ -0,0 +1,320 @@
"""
Unit tests for installer/core/monitor.py
Covers the pure-logic helpers:
_parse_pf_cfg, write_port_forwards_cfg, check_port_conflicts, _build_kubectl_cmd,
_start_port_forwards
"""
from __future__ import annotations
import subprocess
from pathlib import Path
from unittest.mock import MagicMock, patch, call
import pytest
from installer.core.monitor import (
_parse_pf_cfg,
write_port_forwards_cfg,
check_port_conflicts,
_build_kubectl_cmd,
_start_port_forwards,
)
# ---------------------------------------------------------------------------
# _parse_pf_cfg
# ---------------------------------------------------------------------------
def test_parse_pf_cfg_missing_file(tmp_path):
result = _parse_pf_cfg(tmp_path / "nonexistent.cfg")
assert result == []
def test_parse_pf_cfg_empty_file(tmp_path):
cfg = tmp_path / "pf.cfg"
cfg.write_text("")
assert _parse_pf_cfg(cfg) == []
def test_parse_pf_cfg_comments_and_blanks(tmp_path):
cfg = tmp_path / "pf.cfg"
cfg.write_text("# this is a comment\n\n \n")
assert _parse_pf_cfg(cfg) == []
def test_parse_pf_cfg_no_colon(tmp_path):
cfg = tmp_path / "pf.cfg"
cfg.write_text("some line without colon\n")
assert _parse_pf_cfg(cfg) == []
def test_parse_pf_cfg_single_entry(tmp_path):
cfg = tmp_path / "pf.cfg"
cfg.write_text("argocd: local=8080 remote=80 ns=argocd svc=argocd-server address=127.0.0.1\n")
result = _parse_pf_cfg(cfg)
assert len(result) == 1
m = result[0]
assert m["id"] == "argocd"
assert m["local"] == "8080"
assert m["remote"] == "80"
assert m["ns"] == "argocd"
assert m["svc"] == "argocd-server"
assert m["address"] == "127.0.0.1"
def test_parse_pf_cfg_multiple_entries(tmp_path):
cfg = tmp_path / "pf.cfg"
cfg.write_text(
"# Port forwards\n"
"argocd: local=8080 remote=80 ns=argocd svc=argocd-server address=127.0.0.1\n"
"openbao: local=8200 remote=8200 ns=default svc=openbao address=127.0.0.1\n"
)
result = _parse_pf_cfg(cfg)
assert len(result) == 2
assert result[0]["id"] == "argocd"
assert result[1]["id"] == "openbao"
assert result[1]["local"] == "8200"
def test_parse_pf_cfg_token_without_equals(tmp_path):
"""Tokens without '=' should be silently ignored."""
cfg = tmp_path / "pf.cfg"
cfg.write_text("myid: local=9000 badtoken remote=80\n")
result = _parse_pf_cfg(cfg)
assert len(result) == 1
assert result[0]["local"] == "9000"
assert result[0]["remote"] == "80"
# 'badtoken' has no '=' so it's not added
assert "badtoken" not in result[0]
def test_parse_pf_cfg_id_with_extra_whitespace(tmp_path):
cfg = tmp_path / "pf.cfg"
cfg.write_text(" myservice : local=1234 remote=5678\n")
result = _parse_pf_cfg(cfg)
assert len(result) == 1
assert result[0]["id"] == "myservice"
# ---------------------------------------------------------------------------
# write_port_forwards_cfg
# ---------------------------------------------------------------------------
def test_write_port_forwards_cfg_empty(tmp_path):
out = tmp_path / "out.cfg"
write_port_forwards_cfg(out, [])
content = out.read_text()
assert "# Port forward configuration" in content
assert "# Format:" in content
def test_write_port_forwards_cfg_skips_no_id(tmp_path):
out = tmp_path / "out.cfg"
# Mapping string with no id= token
write_port_forwards_cfg(out, ["hostPort=8080;servicePort=80"])
content = out.read_text()
# No entry line should be written (id is empty)
lines = [l for l in content.splitlines() if l and not l.startswith("#")]
assert lines == []
def test_write_port_forwards_cfg_single_mapping(tmp_path):
out = tmp_path / "out.cfg"
mapping = "id=argocd;namespace=argocd;target=svc/argocd-server;hostPort=8080;servicePort=80;address=127.0.0.1"
write_port_forwards_cfg(out, [mapping])
content = out.read_text()
assert "argocd: local=8080 remote=80 ns=argocd svc=argocd-server address=127.0.0.1" in content
def test_write_port_forwards_cfg_target_without_svc_prefix(tmp_path):
out = tmp_path / "out.cfg"
mapping = "id=myapp;namespace=default;target=myapp-service;hostPort=9000;servicePort=9000;address=0.0.0.0"
write_port_forwards_cfg(out, [mapping])
content = out.read_text()
# target doesn't start with svc/, so svc = target as-is
assert "svc=myapp-service" in content
def test_write_port_forwards_cfg_roundtrip(tmp_path):
"""Written file can be parsed back by _parse_pf_cfg."""
out = tmp_path / "pf.cfg"
mappings = [
"id=argocd;namespace=argocd;target=svc/argocd-server;hostPort=8080;servicePort=80;address=127.0.0.1",
"id=openbao;namespace=default;target=svc/openbao;hostPort=8200;servicePort=8200;address=127.0.0.1",
]
write_port_forwards_cfg(out, mappings)
parsed = _parse_pf_cfg(out)
assert len(parsed) == 2
assert parsed[0]["id"] == "argocd"
assert parsed[1]["id"] == "openbao"
assert parsed[0]["local"] == "8080"
assert parsed[1]["remote"] == "8200"
# ---------------------------------------------------------------------------
# check_port_conflicts
# ---------------------------------------------------------------------------
def test_check_port_conflicts_empty():
assert check_port_conflicts([]) == []
def test_check_port_conflicts_no_conflicts():
mappings = [
{"id": "a", "local": "8080"},
{"id": "b", "local": "8200"},
{"id": "c", "local": "9000"},
]
assert check_port_conflicts(mappings) == []
def test_check_port_conflicts_single_conflict():
mappings = [
{"id": "a", "local": "8080"},
{"id": "b", "local": "8080"},
]
result = check_port_conflicts(mappings)
assert len(result) == 1
assert "8080" in result[0]
assert "a" in result[0]
assert "b" in result[0]
def test_check_port_conflicts_multiple_conflicts():
mappings = [
{"id": "a", "local": "8080"},
{"id": "b", "local": "8080"},
{"id": "c", "local": "9000"},
{"id": "d", "local": "9000"},
]
result = check_port_conflicts(mappings)
assert len(result) == 2
def test_check_port_conflicts_missing_local_key():
"""Mappings without 'local' key use '' as port and should not crash."""
mappings = [
{"id": "a"},
{"id": "b"},
]
result = check_port_conflicts(mappings)
# Both have empty port '' — should detect conflict
assert len(result) == 1
def test_check_port_conflicts_missing_id_key():
"""Mappings without 'id' key should not crash."""
mappings = [
{"local": "8080"},
{"local": "8080"},
]
result = check_port_conflicts(mappings)
assert len(result) == 1
assert "?" in result[0]
# ---------------------------------------------------------------------------
# _build_kubectl_cmd
# ---------------------------------------------------------------------------
def test_build_kubectl_cmd_basic():
m = {"ns": "argocd", "svc": "argocd-server", "local": "8080", "remote": "80", "address": "127.0.0.1"}
cmd = _build_kubectl_cmd(m)
assert cmd == ["kubectl", "port-forward", "-n", "argocd", "svc/argocd-server", "8080:80"]
# address is 127.0.0.1 so --address is NOT added
assert "--address" not in cmd
def test_build_kubectl_cmd_non_localhost_address():
m = {"ns": "default", "svc": "myapp", "local": "9000", "remote": "9000", "address": "0.0.0.0"}
cmd = _build_kubectl_cmd(m)
assert "--address" in cmd
assert "0.0.0.0" in cmd
def test_build_kubectl_cmd_svc_prefix_not_doubled():
"""svc/ prefix should only appear once."""
m = {"ns": "default", "svc": "svc/myapp", "local": "9000", "remote": "9000", "address": "127.0.0.1"}
cmd = _build_kubectl_cmd(m)
svc_arg = cmd[4]
assert svc_arg == "svc/myapp"
assert not svc_arg.startswith("svc/svc/")
def test_build_kubectl_cmd_adds_svc_prefix():
"""Service name without svc/ prefix should get one."""
m = {"ns": "default", "svc": "myapp", "local": "9000", "remote": "9000", "address": "127.0.0.1"}
cmd = _build_kubectl_cmd(m)
assert "svc/myapp" in cmd
def test_build_kubectl_cmd_defaults():
"""Missing keys should fall back to defaults."""
cmd = _build_kubectl_cmd({})
assert cmd[0] == "kubectl"
assert "-n" in cmd
assert "default" in cmd # default namespace
def test_build_kubectl_cmd_empty_address():
"""Empty address string should not add --address flag."""
m = {"ns": "default", "svc": "myapp", "local": "9000", "remote": "9000", "address": ""}
cmd = _build_kubectl_cmd(m)
assert "--address" not in cmd
# ---------------------------------------------------------------------------
# _start_port_forwards
# ---------------------------------------------------------------------------
def test_start_port_forwards_empty():
result = _start_port_forwards([])
assert result == []
def test_start_port_forwards_success(capsys):
mapping = {"id": "argocd", "ns": "argocd", "svc": "argocd-server", "local": "8080", "remote": "80", "address": "127.0.0.1"}
mock_proc = MagicMock(spec=subprocess.Popen)
with patch("installer.core.monitor.subprocess.Popen", return_value=mock_proc) as mock_popen:
result = _start_port_forwards([mapping])
assert len(result) == 1
m_out, proc_out = result[0]
assert m_out is mapping
assert proc_out is mock_proc
out = capsys.readouterr().out
assert "argocd" in out
assert "" in out
def test_start_port_forwards_failure(capsys):
mapping = {"id": "broken", "ns": "default", "svc": "nosvc", "local": "9999", "remote": "9999", "address": "127.0.0.1"}
with patch("installer.core.monitor.subprocess.Popen", side_effect=OSError("not found")):
result = _start_port_forwards([mapping])
assert len(result) == 1
_, proc = result[0]
assert proc is None
out = capsys.readouterr().out
assert "" in out
assert "broken" in out
def test_start_port_forwards_verbose(capsys):
mapping = {"id": "test", "ns": "default", "svc": "myapp", "local": "8000", "remote": "8000", "address": "127.0.0.1"}
mock_proc = MagicMock(spec=subprocess.Popen)
with patch("installer.core.monitor.subprocess.Popen", return_value=mock_proc):
_start_port_forwards([mapping], verbose=True)
out = capsys.readouterr().out
assert "[PORT-FWD]" in out
def test_start_port_forwards_multiple(capsys):
mappings = [
{"id": "a", "ns": "default", "svc": "svc-a", "local": "8001", "remote": "80", "address": "127.0.0.1"},
{"id": "b", "ns": "default", "svc": "svc-b", "local": "8002", "remote": "80", "address": "127.0.0.1"},
]
mock_proc = MagicMock(spec=subprocess.Popen)
with patch("installer.core.monitor.subprocess.Popen", return_value=mock_proc):
result = _start_port_forwards(mappings)
assert len(result) == 2
assert result[0][0]["id"] == "a"
assert result[1][0]["id"] == "b"

View File

@ -39,6 +39,13 @@ def test_render_title_no_canvas():
@patch("platform.system", return_value="Darwin") @patch("platform.system", return_value="Darwin")
def test_terminal_console(mock_platform): def test_terminal_console(mock_platform):
import sys
from unittest.mock import MagicMock as _MM
# TerminalConsole requires a real tkinter root; skip when tkinter is mocked.
if isinstance(sys.modules.get("tkinter"), _MM):
pytest.skip("tkinter is mocked in this test run; TerminalConsole needs real tkinter")
root = tk.Tk() root = tk.Tk()
try: try:
console = TerminalConsole(root) console = TerminalConsole(root)
@ -51,3 +58,5 @@ def test_terminal_console(mock_platform):
assert console.text.cget("state") == "disabled" assert console.text.cget("state") == "disabled"
finally: finally:
root.destroy() root.destroy()

View File

@ -37,6 +37,40 @@ def test_run_streaming_cmd_streams_stdout_incrementally():
assert started_at < first_chunk_at[0] assert started_at < first_chunk_at[0]
def test_run_streaming_cmd_nonzero_exit():
rc = run_streaming_cmd([sys.executable, "-c", "import sys; sys.exit(42)"])
assert rc == 42
def test_run_streaming_cmd_string_form():
"""A plain string command is wrapped in bash -lc."""
chunks: list[str] = []
rc = run_streaming_cmd(
f"{sys.executable} -c \"print('strtest')\"",
on_stdout=lambda t: chunks.append(t),
)
assert rc == 0
assert "strtest" in "".join(chunks)
def test_run_streaming_cmd_stdin_text():
"""stdin_text is written to the subprocess stdin."""
chunks: list[str] = []
rc = run_streaming_cmd(
[sys.executable, "-c", "import sys; print(sys.stdin.read().strip())"],
stdin_text="hello_from_stdin",
on_stdout=lambda t: chunks.append(t),
)
assert rc == 0
assert "hello_from_stdin" in "".join(chunks)
def test_run_streaming_cmd_no_callbacks():
"""Running without callbacks should not crash."""
rc = run_streaming_cmd([sys.executable, "-c", "print('silent')"])
assert rc == 0
def test_run_streaming_cmd_keeps_stderr_and_ansi_sequences(): def test_run_streaming_cmd_keeps_stderr_and_ansi_sequences():
stdout_chunks: list[str] = [] stdout_chunks: list[str] = []
stderr_chunks: list[str] = [] stderr_chunks: list[str] = []

View File

@ -105,6 +105,9 @@ def test_navigation_flow_standard(mock_installer):
with patch.object( with patch.object(
mock_installer, "_generate_prole_db_dockerfile", return_value=True mock_installer, "_generate_prole_db_dockerfile", return_value=True
): ):
mock_installer.on_next()
assert mock_installer.pages[mock_installer.page_index][0] == "init_db_build"
mock_installer.on_next() mock_installer.on_next()
assert mock_installer.pages[mock_installer.page_index][0] == "init_password" assert mock_installer.pages[mock_installer.page_index][0] == "init_password"
@ -137,8 +140,11 @@ def test_navigation_flow_missing_deps(mock_installer):
def test_on_prev(mock_installer): def test_on_prev(mock_installer):
# init_password -> database_options # init_password -> init_db_build -> database_options
mock_installer.show_page("init_password") mock_installer.show_page("init_password")
mock_installer.on_prev()
assert mock_installer.pages[mock_installer.page_index][0] == "init_db_build"
mock_installer.on_prev() mock_installer.on_prev()
assert mock_installer.pages[mock_installer.page_index][0] == "database_options" assert mock_installer.pages[mock_installer.page_index][0] == "database_options"