diff --git a/conf/k3d-prole-new.cfg b/conf/k3d-prole-new.cfg deleted file mode 100644 index 563a694..0000000 --- a/conf/k3d-prole-new.cfg +++ /dev/null @@ -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 diff --git a/conf/k3d-prole.cfg b/conf/k3d-prole.cfg deleted file mode 100644 index de5b867..0000000 --- a/conf/k3d-prole.cfg +++ /dev/null @@ -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 diff --git a/conf/k3s-hosts-prole.cfg b/conf/k3s-hosts-prole.cfg deleted file mode 100644 index 3e8bf80..0000000 --- a/conf/k3s-hosts-prole.cfg +++ /dev/null @@ -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. diff --git a/conf/port-forwards.cfg b/conf/port-forwards.cfg deleted file mode 100644 index 011e71a..0000000 --- a/conf/port-forwards.cfg +++ /dev/null @@ -1,12 +0,0 @@ -# Port forward configuration for Prole services (generated). -# Format: id: 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-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 diff --git a/conf/port-mapping.cfg b/conf/port-mapping.cfg index 986985d..ec67e3a 100644 --- a/conf/port-mapping.cfg +++ b/conf/port-mapping.cfg @@ -1,3 +1,11 @@ # Port mappings for Prole Tools (generated). # Format: key: local=... remote=... ns=... svc=... address=... +argocd: local=8081 remote=80 ns=argocd svc=argocd-server address=0.0.0.0 +garage: local=3900 remote=3900 ns=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 diff --git a/conf/postgresql/pg_hba.conf b/conf/postgresql/pg_hba.conf deleted file mode 100644 index 3b08826..0000000 --- a/conf/postgresql/pg_hba.conf +++ /dev/null @@ -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 diff --git a/conf/postgresql/pg_ident.conf b/conf/postgresql/pg_ident.conf deleted file mode 100644 index f5225f2..0000000 --- a/conf/postgresql/pg_ident.conf +++ /dev/null @@ -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 diff --git a/conf/postgresql/postgresql.conf b/conf/postgresql/postgresql.conf deleted file mode 100644 index 7b1772d..0000000 --- a/conf/postgresql/postgresql.conf +++ /dev/null @@ -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 diff --git a/conf/prole.cfg b/conf/prole.cfg index a32eafd..40c2c2a 100644 --- a/conf/prole.cfg +++ b/conf/prole.cfg @@ -1,21 +1,36 @@ ; 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. [User] ; User-editable values; derived values below reference these by default. -NAMESPACE = prole-db-a0001 -PROLE_CONF = ${PROLE_HOME}/conf +NAMESPACE = prole-db001 +PROLE_CONF = /Users/chrisfu/dev/prole/conf PROLE_DATA = /Users/chrisfu/.prole/data -PROLE_HOME = /Users/chrisfu/dev/prole +PROLE_HOME = mock_val PROLE_LOGS = /opt/prole/logs/chrisfu -PROLE_SERVICE = ${PROLE_HOME}/etc +PROLE_SERVICE = /Users/chrisfu/dev/prole/etc SERVICE_NAMESPACE = default [Inputs] ; Screen-scoped inputs used for unattended replays (-S) build.deploy_env = Dev 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.auto_install_missing = true dependencies.brew.install = true @@ -27,40 +42,41 @@ 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.local_path = /Users/chrisfu disk_selection.removable_mount = -env_setup.NAMESPACE = prole-hq0-db0 +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 = service -init_cluster.deployment_target = prole-service-cluster +init_cluster.cluster_env = dev +init_cluster.deployment_target = prole-dev-cluster init_cluster.gitops_enabled = false -init_cluster.k3s_server_url = https://myrddin.prole.org:6443 -init_cluster.k3s_token = ${OPENBAO:REDACTED} -init_cluster.kerberos_enabled = true -init_cluster.mode = k3s +init_cluster.k3s_server_url = +init_cluster.k3s_token = +init_cluster.kerberos_enabled = false +init_cluster.mode = k3d 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 = prole-hq0-db0 -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.db_namespace = ${NAMESPACE} +init_password.db_password = ${OPENBAO:kv/prole/prole-db001/db#password} +init_password.db_password_confirm = ${OPENBAO:kv/prole/prole-db001/db#password} +init_password.db_username = root init_password.generate_ssh_key = true init_scripts.run_scripts = true -kerberos_config.enabled = true -kerberos_config.kdc = 10.0.0.3 -kerberos_config.password = ${OPENBAO:kv/prole/${NAMESPACE}/kerberos#password} -kerberos_config.realm = PROLE.ORG -kerberos_config.test_connection = true -kerberos_config.user = chrisfu +kerberos_config.enabled = False +kerberos_config.init_authority = false +kerberos_config.kdc = 10.0.0.205 +kerberos_config.password = ${OPENBAO:kv/prole/prole-db001/kerberos#password} +kerberos_config.realm = +kerberos_config.test_connection = false +kerberos_config.user = administrator network_scan.run = true ollama_config.model = ollama_config.server_host = @@ -68,41 +84,53 @@ ollama_config.server_port = 11434 [Global] ; 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_PASSWORD = ${OPENBAO:kv/prole/${NAMESPACE}/db#password} -DEPLOYMENT_MODE = k3s -DEPLOYMENT_TARGET = prole-service-cluster +DB_PASSWORD = ${OPENBAO:kv/prole/prole-db001/db#password} +DEPLOYMENT_MODE = k3d +DEPLOYMENT_TARGET = knoe-dev-cluster +DOCKER_IMPORT_DIR = NAMESPACE = ${NAMESPACE} -PROLE_DB_USER = chrisfu +PROLE_DB_USER = root PROLE_HOME = ${PROLE_HOME} -PROLE_K3S_SERVER = https://myrddin.prole.org:6443 -PROLE_K3S_TOKEN = ${OPENBAO:REDACTED} +PROLE_K3S_SERVER = +PROLE_K3S_TOKEN = ${OPENBAO:kv/prole/prole-db001/k3s#token} 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] -; No configuration values captured yet for this section. +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_INFRASTRUCTURE = /Users/chrisfu/dev/prole/infrastructure +ANSIBLE_INVENTORY = /Users/chrisfu/dev/prole/infrastructure/inventory ANSIBLE_REALM = PROLE.ORG ANSIBLE_TOPOLOGY = {"domain":"prole.org","realm":"PROLE.ORG","internal_records":{"aventage.prole.org":"10.0.0.206","fairyland.prole.org":"10.0.0.208","loghost.prole.org":"10.0.0.3","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_AUTO_DETECTED = 10.0.0.3 -KERBEROS_AUTO_ENABLED = True +KDC_AUTO_DETECTED = 10.0.0.205 +KERBEROS_AUTO_ENABLED = False [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] +NAMESPACE = prole-db001 PROLE_CONF = ${PROLE_CONF} PROLE_DATA = ${PROLE_DATA} PROLE_HOME = ${PROLE_HOME} @@ -113,70 +141,59 @@ PROLE_SERVICE = ${PROLE_SERVICE} ; No configuration values captured yet for this section. [Kerberos Authentication] -AD_PORT_FORWARD = 1 -AD_PROXY_HOST_NETWORK = 1 -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 +PASSWORD = ${OPENBAO:kv/prole/prole-db001/kerberos#password} +STATUS = Skipped [Ollama] ; No configuration values captured yet for this section. [Optional Features] -AT_REST_ENCRYPTION_ENABLED = True -GITOPS_ENABLED = False -KERBEROS_ENABLED = True -SUPABASE_ENABLED = False +AT_REST_ENCRYPTION_ENABLED = true +GITOPS_ENABLED = false +KERBEROS_ENABLED = false +SUPABASE_ENABLED = false [GitOps] -; No configuration values captured yet for this section. +STATUS = Skipped [Database Creation] +DB_NAME = prole-db001 +DB_USER = root NAMESPACE = ${NAMESPACE} [Initialize Cluster] -ENVIRONMENT = service -K3S_SERVER_URL = https://myrddin.prole.org:6443 -K3S_TOKEN = ${OPENBAO:REDACTED} +; No configuration values captured yet for this section. [Dev Cluster (k3d)] -CLUSTER_ENV = dev -DISPLAY_NAME = prole-dev-cluster -KUBECTL_CONTEXT = prole-k3s +CLUSTER_ENV = k3d-knoe-dev-cluster +DISPLAY_NAME = knoe-dev-cluster +KUBECTL_CONTEXT = dev MODE = k3d [Service Cluster (k3s)] CLUSTER_ENV = prole-service-cluster DISPLAY_NAME = prole-service-cluster -K3S_SERVER_URL = https://myrddin.prole.org:6443 -K3S_TOKEN = ${OPENBAO:REDACTED} +K3S_SERVER_URL = +K3S_TOKEN = ${OPENBAO:kv/prole/prole-db001/k3s#token} MODE = k3s PIPELINE_URL = http://127.0.0.1:8080 [Prod Cluster (k8s)] -ARTIFACTS_DIR = /Users/chrisfu/dev/prole/data/staging +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 +; No configuration values captured yet for this section. [Initialization Scripts] ; No configuration values captured yet for this section. [Deployment] -MODE = k3s -TARGET = prole-service-cluster +MODE = k3d +TARGET = prole-dev-cluster [Install] -; No configuration values captured yet for this section. +STATUS = Finished diff --git a/conf/prole.cfg.k3s b/conf/prole.cfg.k3s deleted file mode 100644 index fad6441..0000000 --- a/conf/prole.cfg.k3s +++ /dev/null @@ -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 diff --git a/deploy/opentofu/k3s/manifests/prole/db-manager-deployment.yaml b/deploy/opentofu/k3s/manifests/prole/db-manager-deployment.yaml deleted file mode 100644 index 6a27e13..0000000 --- a/deploy/opentofu/k3s/manifests/prole/db-manager-deployment.yaml +++ /dev/null @@ -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 diff --git a/deploy/opentofu/k3s/manifests/prole/db-manager-service.yaml b/deploy/opentofu/k3s/manifests/prole/db-manager-service.yaml deleted file mode 100644 index 48dd7c4..0000000 --- a/deploy/opentofu/k3s/manifests/prole/db-manager-service.yaml +++ /dev/null @@ -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 diff --git a/env.sh b/env.sh index 9d6c294..9793846 100755 --- a/env.sh +++ b/env.sh @@ -8,7 +8,7 @@ export PROLE_CONF="/Users/chrisfu/dev/prole/conf" export PROLE_DATA="/Users/chrisfu/.prole/data" export PROLE_LOGS="/opt/prole/logs/chrisfu" 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.) _prole_add_path() { case ":${PATH}:" in *":$1:"*) ;; *) PATH="$1:${PATH:-}" ;; esac; } diff --git a/etc/home.sh b/etc/home.sh new file mode 100755 index 0000000..df180ed --- /dev/null +++ b/etc/home.sh @@ -0,0 +1,2 @@ +#!/bin/bash +echo hometest diff --git a/etc/init_certmgr.sh b/etc/init_certmgr.sh index 04a62c5..7e7c047 100755 --- a/etc/init_certmgr.sh +++ b/etc/init_certmgr.sh @@ -47,7 +47,9 @@ if [[ -n "${COMMON_CORE_PARSE_ERROR:-}" ]]; then fi 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" CERTMGR_VERSION="${CERTMGR_VERSION:-v1.14.6}" @@ -84,38 +86,16 @@ ensure_namespace() { fi } -_patch_manifest_namespace() { - local ns="$1" - 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() { + curl -fsSL "$CERTMGR_RELEASE_URL" | kubectl apply -f - } -_apply_manifest() { - curl -fsSL "$CERTMGR_RELEASE_URL" | _patch_manifest_namespace "$CERTMGR_NAMESPACE" | 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" | _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() { @@ -143,8 +123,12 @@ restart() { action_update() { ensure_tools ensure_namespace - _apply_manifest - wait_rollout + 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" } diff --git a/etc/init_cloudnative_pg.sh b/etc/init_cloudnative_pg.sh index 2956fd4..914cd09 100755 --- a/etc/init_cloudnative_pg.sh +++ b/etc/init_cloudnative_pg.sh @@ -577,6 +577,137 @@ apply_prole_manifest_file() { 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() { echo "Applying CloudNative-PG cluster and related resources ..." local image_override="${CNPG_IMAGE:-${PROLE_DB_IMAGE:-}}" @@ -726,6 +857,7 @@ wait_for_cnpg_pods() { echo " Timeout: ${timeout}s" local last_feedback=0 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 local now elapsed 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) 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 echo "ERROR: Some CNPG pods are in Error or CrashLoopBackOff state:" >&2 echo "$pods" | grep -E "Error|CrashLoopBackOff" >&2 return 1 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) local running_pods running_pods=$(kubectl -n "$NAMESPACE" get pods --no-headers 2>/dev/null \ @@ -1432,6 +1598,11 @@ initialize() { fi 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 if ! wait_for_cnpg_pods 300; then @@ -1486,6 +1657,7 @@ deploy_cluster() { generate_tls_if_missing echo "Deploying $image to cluster $CNPG_CLUSTER_NAME in namespace $NAMESPACE..." + _ensure_prole_db_image || true apply_cnpg_cluster_manifest "$CNPG_MANIFEST" local current_image @@ -1503,6 +1675,20 @@ case "$ACTION" in recreate) ensure_tools "$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" ;; create) @@ -1513,7 +1699,20 @@ case "$ACTION" in delete) ensure_tools 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) ensure_tools diff --git a/etc/init_common_services.sh b/etc/init_common_services.sh index a5a0e19..b6129db 100755 --- a/etc/init_common_services.sh +++ b/etc/init_common_services.sh @@ -147,6 +147,80 @@ _resolve_prole_data() { 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() { local 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 return 0 fi - local cluster_name="${K3D_CLUSTER_NAME:-prole-dev-cluster}" - echo " Importing all images into k3d cluster '$cluster_name' (bulk) ..." - local -a tar_array=() - while IFS= read -r line; do - [[ -n "$line" ]] && tar_array+=("$line") + local cluster_name="${K3D_CLUSTER_NAME:-knoe-dev-cluster}" + + # ── Step 1: build indexed arrays of tar paths + resolved image names ── + local -a tar_paths=() + 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" - if [[ ${#tar_array[@]} -gt 0 ]]; then - if k3d image import "${tar_array[@]}" -c "$cluster_name" >/dev/null 2>&1; then - echo " [OK] Bulk import of ${#tar_array[@]} images successful." - else - echo " [WARN] Bulk import failed; falling back to sequential." >&2 - for tar_file in "${tar_array[@]}"; do - echo " Importing $(basename "$tar_file") into k3d cluster '$cluster_name' ..." - if k3d image import "$tar_file" -c "$cluster_name" >/dev/null 2>&1; then - echo " [OK] $(basename "$tar_file")" - else - echo " [WARN] Failed to import $(basename "$tar_file"); image will be pulled at deploy time." >&2 - fi - done + 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&2 + local _idx + for (( _i=0; _i&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) diff --git a/etc/init_k8s.sh b/etc/init_k8s.sh index 2df81e6..e5d3266 100755 --- a/etc/init_k8s.sh +++ b/etc/init_k8s.sh @@ -16,7 +16,7 @@ source "$SCRIPT_DIR/prole_cfg.sh" VERBOSE=false ENVIRONMENT="prod" HOST="" -CLUSTER_NAME_DEFAULT="prole-dev-cluster" +CLUSTER_NAME_DEFAULT="knoe-dev-cluster" CLUSTER_PORT_DEFAULT="6443" usage() { diff --git a/etc/init_kdc.sh b/etc/init_kdc.sh index 9f40a87..317df7c 100755 --- a/etc/init_kdc.sh +++ b/etc/init_kdc.sh @@ -355,7 +355,7 @@ build_prole_kdc_image() { if [[ -z "$PROLE_KDC_REGISTRY_HOST" && "$(normalized_mode)" == "k3d" ]]; 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}" k3d image import "$local_tag" -c "$cluster_name" >/dev/null 2>&1 || true fi diff --git a/etc/init_registry.sh b/etc/init_registry.sh index 4b8fce8..e6a10d9 100755 --- a/etc/init_registry.sh +++ b/etc/init_registry.sh @@ -255,7 +255,7 @@ apply_k8s() { } 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_port="5000" local registries_yaml @@ -291,7 +291,7 @@ apply_registry() { if ! k3d registry list prole-registry >/dev/null 2>&1; then k3d registry create prole-registry --port 5000 || true 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 echo "Connecting prole-registry to $cluster_name ..." # k3d v5+ uses 'k3d-NAME' as container name for registries diff --git a/etc/init_service_layer.sh b/etc/init_service_layer.sh index 3594f7e..75ff209 100755 --- a/etc/init_service_layer.sh +++ b/etc/init_service_layer.sh @@ -166,7 +166,7 @@ deploy_service_layer() { ensure_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 start|initialize|update|reload) argocd_action="update" ;; restart) argocd_action="restart" ;; @@ -191,14 +191,6 @@ deploy_service_layer() { *) openbao_action="update" ;; 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 start|initialize|update|reload) garage_action="start" ;; restart) garage_action="restart" ;; @@ -230,8 +222,8 @@ deploy_service_layer() { OPENBAO_NAMESPACE="$ns" SERVICE_NAMESPACE="$ns" \ "$SCRIPT_DIR/init_openbao.sh" -n "$ns" "$openbao_action" || rc=$? - CERTMGR_NAMESPACE="$ns" SERVICE_NAMESPACE="$ns" \ - "$SCRIPT_DIR/init_certmgr.sh" -n "$ns" "$certmgr_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" \ "$SCRIPT_DIR/init_garage_store.sh" "$garage_action" || rc=$? diff --git a/etc/k3d_route_fix.sh b/etc/k3d_route_fix.sh index b388c4c..9207162 100755 --- a/etc/k3d_route_fix.sh +++ b/etc/k3d_route_fix.sh @@ -15,7 +15,7 @@ source "$SCRIPT_DIR/prole_cfg.sh" log() { printf '%s\n' "$*"; } 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:-}}}" if [[ -z "$ad_dc_ip" ]]; then diff --git a/etc/prole_cfg.sh b/etc/prole_cfg.sh index 39ddf68..86ca5d6 100644 --- a/etc/prole_cfg.sh +++ b/etc/prole_cfg.sh @@ -176,6 +176,7 @@ prole_normalize_mode() { service|k3s|k3s-*) echo "k3s"; return 0 ;; prod|production|k8s|k8s-*) echo "k8s"; 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-prod-cluster) echo "k8s"; return 0 ;; esac diff --git a/etc/repair_pipeline.sh b/etc/repair_pipeline.sh index 0ecda96..ffb7ac5 100755 --- a/etc/repair_pipeline.sh +++ b/etc/repair_pipeline.sh @@ -143,7 +143,7 @@ cluster_ready() { } k3d_cluster_ready() { - local cluster="${K3D_CLUSTER:-prole-dev-cluster}" + local cluster="${K3D_CLUSTER:-knoe-dev-cluster}" if have k3d; then if k3d cluster list --no-headers 2>/dev/null | grep -q "^${cluster}[[:space:]]"; then return 0 diff --git a/etc/status_common_services.sh b/etc/status_common_services.sh index f09bd8b..e9a093c 100755 --- a/etc/status_common_services.sh +++ b/etc/status_common_services.sh @@ -80,14 +80,14 @@ fi ARGOCD_NS="${ARGOCD_NAMESPACE:-argocd}" 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}" if [ -z "$KONG_NS" ]; then KONG_NS="default" fi if [ -z "$CERTMGR_NS" ]; then - CERTMGR_NS="default" + CERTMGR_NS="cert-manager" fi if ! command -v kubectl >/dev/null 2>&1; then @@ -275,64 +275,257 @@ fi if [ $missing -gt 0 ]; then echo "" - echo "Status: FAILED ($missing resources missing)" - exit 1 + # ── Attempt to repair a completely absent cert-manager before failing ────── + 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 +# ── 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 - repairable=() - fail_all=() - for comp in argocd openbao opentofu garage auth kong certmgr; do - if [[ "$comp" == "auth" && "$ENABLE_KERBEROS" != "1" ]]; then - continue + + # ── Round 1: delete blocked pods ────────────────────────────────────────── + echo "" + echo "== Repair Round 1: replacing blocked pods ==" + 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 - hc=${HEALTHY_COUNT["$comp"]:-0} - bc=${BLOCKED_COUNT["$comp"]:-0} - if [[ "$bc" -gt 0 && "$hc" -gt 0 ]]; then - repairable+=("$comp") - elif [[ "$bc" -gt 0 && "$hc" -eq 0 ]]; then - fail_all+=("$comp") + done + echo " Waiting for pod replacement (15s)..." + sleep 15 + _recheck_pods + + 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 done - if [ ${#fail_all[@]} -gt 0 ]; then - echo "" - echo "Repair skipped: all pods blocked for ${fail_all[*]}." - echo "Action: Deploy Missing Services." - echo "" - echo "Status: NOTICE (blocked pods detected)" - exit 2 + if [[ -x "$SCRIPT_DIR/init_service_layer.sh" ]]; then + echo " [Round 3] Re-initializing service layer ..." + _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 - 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 "== Repair ==" - echo "Repairing blocked pods for: ${repairable[*]}" - 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 + echo "Status: OK (resolved in repair round 3)" + exit 0 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 echo "" diff --git a/installer/core/actions.py b/installer/core/actions.py index d71d5ea..63872d8 100644 --- a/installer/core/actions.py +++ b/installer/core/actions.py @@ -949,13 +949,13 @@ class ProleInstallerBase: elif mode == "k3d": # Ensure k3d dev clusters have a resolvable KUBECONFIG. - cluster_name = self._get_input("init_cluster.deployment_target", "prole-dev-cluster") - if not cluster_name or cluster_name == "prole-dev-cluster": + cluster_name = self._get_input("init_cluster.deployment_target", "knoe-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 cluster_name = getattr(self, "selected_k3d_cluster", None) if hasattr(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: subprocess.run( @@ -2501,7 +2501,12 @@ class ProleSilentInstaller(ProleInstallerBase): "PROLE_OPENTOFU_URL": _default_opentofu_pipeline_url(), "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( "Global", "DB_PASSWORD", db_pw, "db", "password" ) @@ -2591,8 +2596,8 @@ class ProleSilentInstaller(ProleInstallerBase): sections["Dev Cluster (k3d)"] = { **self.prole_cfg_data.get("Dev Cluster (k3d)", {}), "MODE": "k3d", - "CLUSTER_ENV": "k3d-prole-dev-cluster", - "DISPLAY_NAME": "prole-dev-cluster", + "CLUSTER_ENV": "k3d-knoe-dev-cluster", + "DISPLAY_NAME": "knoe-dev-cluster", "KUBECTL_CONTEXT": self._get_input("init_cluster.cluster_env", ""), } sections["Service Cluster (k3s)"] = { @@ -2993,7 +2998,7 @@ class ProleSilentInstaller(ProleInstallerBase): env_key = _normalize_cluster_env( self._get_input("init_cluster.cluster_env", "dev") ) - cluster_name = "prole-dev-cluster" + cluster_name = "knoe-dev-cluster" # Check if image already exists rc_inspect = subprocess.run( @@ -3047,7 +3052,7 @@ class ProleSilentInstaller(ProleInstallerBase): self.log("[OK] Build successful.") push_ok = True if env_key == "dev": - cluster_name = "prole-dev-cluster" + cluster_name = "knoe-dev-cluster" self.log(f"Importing image to {cluster_name}...") self._run_cmd( ["k3d", "image", "import", image_name, "-c", cluster_name] @@ -3141,7 +3146,7 @@ class ProleSilentInstaller(ProleInstallerBase): if not self.controller.check_docker_running(): raise Exception("Docker is not running.") - cluster_name = "prole-dev-cluster" + cluster_name = "knoe-dev-cluster" res = subprocess.run( ["k3d", "cluster", "list", "--no-headers"], capture_output=True, @@ -3542,7 +3547,6 @@ class ProleSilentInstaller(ProleInstallerBase): [ ("init_prole-db-backup.sh", ["start"], False), ("init_kong.sh", ["start"], False), - ("init_db_manager.sh", ["start"], False), ("init_monitoring.sh", ["initialize"], False), ] ) @@ -3821,7 +3825,7 @@ class ProleSilentInstaller(ProleInstallerBase): if not self.controller.check_docker_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]) if rc != 0: self.err( diff --git a/installer/core/env.py b/installer/core/env.py index 0a1fc98..0dc785d 100644 --- a/installer/core/env.py +++ b/installer/core/env.py @@ -844,7 +844,7 @@ def _normalize_cluster_env(env: str | None) -> str: return "" s = str(env).strip().lower() 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("prole-dev-") ): @@ -874,7 +874,7 @@ def _cluster_env_radio_value(env: str | None) -> str: def _deployment_target_label(env: str | None) -> str: key = _normalize_cluster_env(env) if key == "dev": - return "prole-dev-cluster" + return "knoe-dev-cluster" if key == "service": return "prole-service-cluster" if key == "prod": @@ -1610,8 +1610,9 @@ def _render_prole_cfg( def first_non_empty(*vals: str) -> str: for val in vals: - if val: - return val + v = str(val or "").strip() + if v and not (v.startswith("${") and v.endswith("}")): + return v return "" def get_input(key: str) -> str: @@ -2066,16 +2067,6 @@ def _build_required_port_forwards( "TCP", "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: diff --git a/installer/core/milestones.py b/installer/core/milestones.py index 3140c9f..200dead 100644 --- a/installer/core/milestones.py +++ b/installer/core/milestones.py @@ -438,7 +438,7 @@ class ClusterLifecycleMilestone(Milestone): self.logger.error("Docker is not running.") return - cluster_name = "prole-dev-cluster" + cluster_name = "knoe-dev-cluster" res = subprocess.run( ["k3d", "cluster", "list", "--no-headers"], capture_output=True, @@ -665,7 +665,6 @@ class InitializationScriptsMilestone(Milestone): [ "init_prole-db-backup.sh", "init_kong.sh", - "init_db_manager.sh", "init_monitoring.sh", ] ) @@ -692,7 +691,7 @@ class InitializationScriptsMilestone(Milestone): args = mode_args + ["initialize"] elif script == "init_prole-db-backup.sh": args = mode_args + ["start"] - elif script in ("init_kong.sh", "init_db_manager.sh"): + elif script == "init_kong.sh": args = mode_args + ["start"] elif script in ("init_monitoring.sh", "init_nginx_ingress.sh"): args = mode_args + ["initialize"] diff --git a/installer/milestone.py b/installer/milestone.py index 1753f83..5f83e16 100644 --- a/installer/milestone.py +++ b/installer/milestone.py @@ -128,7 +128,7 @@ class Milestone(ABC): if not (env.get("KUBECONFIG") or "").strip(): import subprocess as _sp - cluster_name = "prole-dev-cluster" + cluster_name = "knoe-dev-cluster" try: _sp.run( [ diff --git a/installer/ncurses_installer.py b/installer/ncurses_installer.py index 59b7827..7810dfa 100644 --- a/installer/ncurses_installer.py +++ b/installer/ncurses_installer.py @@ -52,9 +52,9 @@ class ProleNcursesInstaller: self.selected_local_path = None # Environment/config variables - self.cluster_env = "k3d-prole-dev-cluster" + self.cluster_env = "k3d-knoe-dev-cluster" self.cluster_environments = [ - "prole-dev-cluster", + "knoe-dev-cluster", "prole-service-cluster", "prole-prod-cluster", ] @@ -386,7 +386,7 @@ class ProleNcursesInstaller: return True 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 pass # We could implement sub-selection elif key == curses.KEY_NPAGE: # Page Down to scroll context list? @@ -851,7 +851,7 @@ class ProleNcursesInstaller: y += 1 selected_env = self.cluster_environments[self.selected_env_index] - if selected_env == "prole-dev-cluster": + if selected_env == "knoe-dev-cluster": win.render_text( y, 4, diff --git a/installer/ui/screens/__init__.py b/installer/ui/screens/__init__.py index 62e25ec..9e0dafd 100644 --- a/installer/ui/screens/__init__.py +++ b/installer/ui/screens/__init__.py @@ -346,8 +346,8 @@ class ProleInstaller( ("System Environment", "env_setup"), ("Cluster Environment", "init_cluster"), ("Database Options", "database_options"), - ("Database Creation", "init_password"), ("Docker Build", "init_db_build"), + ("Database Creation", "init_password"), ("Initialization Scripts", "init_scripts"), ("Kerberos Authentication", "kerberos_config"), ("GitOps", "gitops_config"), @@ -533,7 +533,7 @@ class ProleInstaller( # Cluster / k3d variables 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.selected_k3d_cluster = tk.StringVar() try: diff --git a/installer/ui/screens/cfg.py b/installer/ui/screens/cfg.py index c58ecf2..ff82b54 100644 --- a/installer/ui/screens/cfg.py +++ b/installer/ui/screens/cfg.py @@ -82,8 +82,12 @@ class ConfigMixin: "DB_HOST_PORT": (self.db_host_port.get() or "5432").strip(), "PROLE_OPENTOFU_URL": _default_opentofu_pipeline_url(), } - # Add any other values already in self.prole_cfg_data['Global'] - globals_to_save.update(self.prole_cfg_data.get("Global", {})) + # Merge any other values already present in existing Global without overriding explicit UI values + 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( "Global", "DB_PASSWORD", self.db_password.get(), "db", "password" ) @@ -135,7 +139,7 @@ class ConfigMixin: **self.prole_cfg_data.get("Dev Cluster (k3d)", {}), "MODE": "k3d", "CLUSTER_ENV": "dev", - "DISPLAY_NAME": "prole-dev-cluster", + "DISPLAY_NAME": "knoe-dev-cluster", "KUBECTL_CONTEXT": (self.selected_kubectx.get() or "").strip(), } 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 _set_bool( "init_db_build.run_build", diff --git a/installer/ui/screens/cluster.py b/installer/ui/screens/cluster.py index d52ade0..d4a295a 100644 --- a/installer/ui/screens/cluster.py +++ b/installer/ui/screens/cluster.py @@ -1182,7 +1182,7 @@ class ClusterScreenMixin: """Verify the config then write the values to prole.cfg.""" env_key = self._cluster_env_key() 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: messagebox.showwarning( "Validation", "Please select or create a k3d cluster." @@ -1266,7 +1266,7 @@ class ClusterScreenMixin: cluster_ok = False 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: res = subprocess.run( ["k3d", "cluster", "list", "--no-headers"], @@ -1457,7 +1457,7 @@ class ClusterScreenMixin: return # 2. Manage or verify cluster - cluster_name = "prole-dev-cluster" + cluster_name = "knoe-dev-cluster" res = subprocess.run( ["k3d", "cluster", "list", "--no-headers"], capture_output=True, @@ -1612,20 +1612,6 @@ class ClusterScreenMixin: 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") # Parse per-component traffic light status from [OK]/[FAIL] lines diff --git a/installer/ui/screens/database.py b/installer/ui/screens/database.py index 96979c3..9f4d592 100644 --- a/installer/ui/screens/database.py +++ b/installer/ui/screens/database.py @@ -1282,7 +1282,7 @@ class DatabaseScreenMixin: ) # Import to k3d - cluster_name = "prole-dev-cluster" + cluster_name = "knoe-dev-cluster" self._db_build_console.write( f"Importing image to {cluster_name} ({import_tag})...\n" ) diff --git a/installer/ui/screens/database_options.py b/installer/ui/screens/database_options.py index 708960d..6fe935d 100644 --- a/installer/ui/screens/database_options.py +++ b/installer/ui/screens/database_options.py @@ -14,14 +14,23 @@ class DatabaseOptionsScreenMixin: """Mixin for the Database Options screen.""" 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_distribution = tk.StringVar(value="percona") - self.db_version_type = tk.StringVar(value="latest") # stable, current, latest, v18 - self.db_selected_version = tk.StringVar(value="17") - + _saved_dist = _saved.get("database_options.distribution", "percona") + self.db_distribution = tk.StringVar(value=_saved_dist or "percona") + _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 self.db_extensions = {} - + self.extensions_list = [ {"id": "postgis", "name": "PostGIS", "description": "Spatial and geographic objects for PostgreSQL"}, {"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": "pgbadger", "name": "pgBadger", "description": "Log analyzer (Package only, no extension)", "no_extension": True}, ] - - # Default enabled extensions - default_enabled = ["postgis", "pgvector", "pgcrypto", "pgaudit", "pg_repack", "pg_stat_statements", "pg_buffercache", "pg_freespacemap", "pgrowlocks", "postgres_fdw", "dblink"] - + + # 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", "pg_stat_monitor", "pgbadger"] + 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_versions_data = {} self._load_database_versions() diff --git a/installer/ui/screens/deploy.py b/installer/ui/screens/deploy.py index 77d818b..052dd36 100644 --- a/installer/ui/screens/deploy.py +++ b/installer/ui/screens/deploy.py @@ -471,7 +471,7 @@ class DeployScreenMixin: env_combo.pack(side="left") env_help = tk.Label( 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", fg="#888888", font=("Helvetica", 9), @@ -776,7 +776,7 @@ kerberos.kdc={self.kerberos_kdc.get()} self.update_deploy_step_status( "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( "Import image to k3d cluster (Dev only)", "completed" ) diff --git a/installer/ui/screens/docker.py b/installer/ui/screens/docker.py index e1a89ab..1083890 100644 --- a/installer/ui/screens/docker.py +++ b/installer/ui/screens/docker.py @@ -500,7 +500,7 @@ class DockerScreenMixin: if not _push_docker_image(remote_tag): 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).""" version = self.get_prole_db_version() image_name = f"prole-db:{version}" @@ -580,7 +580,7 @@ class DockerScreenMixin: else: 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.""" result = subprocess.run( ["k3d", "cluster", "list"], capture_output=True, text=True diff --git a/installer/ui/screens/navigation.py b/installer/ui/screens/navigation.py index c62f1aa..bf627e8 100644 --- a/installer/ui/screens/navigation.py +++ b/installer/ui/screens/navigation.py @@ -328,13 +328,13 @@ class NavigationMixin: self.show_page("init_cluster") return if current_id == "init_password": - self.show_page("database_options") + self.show_page("init_db_build") return if current_id == "init_db_build": - self.show_page("init_password") + self.show_page("database_options") return if current_id == "init_scripts": - self.show_page("init_db_build") + self.show_page("init_password") return if current_id == "kerberos_config": self.show_page("init_scripts") @@ -488,10 +488,7 @@ class NavigationMixin: return if current_id == "init_db_build": - if self.kerberos_enabled.get(): - self.show_page("kerberos_config") - else: - self.show_page("init_scripts") + self.show_page("init_password") return if current_id == "build": @@ -519,7 +516,7 @@ class NavigationMixin: if current_id == "database_options": if self._generate_prole_db_dockerfile(): - self.show_page("init_password") + self.show_page("init_db_build") return if current_id == "init_scripts": @@ -527,7 +524,10 @@ class NavigationMixin: "Completed" if getattr(self, "_scripts_success", False) else "Attempted" ) 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 if current_id == "kerberos_config": diff --git a/installer/ui/screens/services.py b/installer/ui/screens/services.py index bdea3b8..7af8184 100644 --- a/installer/ui/screens/services.py +++ b/installer/ui/screens/services.py @@ -160,7 +160,6 @@ class ServicesScreenMixin: ] scripts.append(("Prole DB Backup", "init_prole-db-backup.sh")) scripts.append(("Kong API Gateway", "init_kong.sh")) - scripts.append(("DB Manager", "init_db_manager.sh")) scripts.append(("Monitoring", "init_monitoring.sh")) mode = self._deployment_mode() if mode != "k3d": @@ -319,7 +318,7 @@ class ServicesScreenMixin: "Skipping common services initialization because previous steps failed.\n" ) - # 2. init_cloudnative_pg.sh initialize + # 3. init_cloudnative_pg.sh initialize if overall_success: script = "init_cloudnative_pg.sh" _select_tab(script) @@ -370,7 +369,7 @@ class ServicesScreenMixin: "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: script = "init_prole-db-backup.sh" _select_tab(script) @@ -417,7 +416,7 @@ class ServicesScreenMixin: "Skipping Prole DB backup because previous steps failed.\n" ) - # 4. init_kong.sh start + # 5. init_kong.sh start if overall_success: script = "init_kong.sh" if script in self.script_consoles: @@ -466,56 +465,7 @@ class ServicesScreenMixin: "Skipping Kong because previous steps failed.\n" ) - # 5. init_db_manager.sh start - 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 + # 7. init_monitoring.sh initialize if overall_success: script = "init_monitoring.sh" _select_tab(script) @@ -574,7 +524,7 @@ class ServicesScreenMixin: "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": script = "init_nginx_ingress.sh" _select_tab(script) @@ -622,7 +572,7 @@ class ServicesScreenMixin: "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": script = "init_port_forwards.sh" if script in self.script_consoles: @@ -967,7 +917,7 @@ class ServicesScreenMixin: ) self._canvas_items.append(mode_label) mode_values = [ - "prole-dev-cluster", + "knoe-dev-cluster", "prole-service-cluster", "prole-prod-cluster", ] @@ -1784,6 +1734,6 @@ class ServicesScreenMixin: 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() diff --git a/k8s/prole/prole-db-recovery.yaml.tpl b/k8s/prole/prole-db-recovery.yaml.tpl index c003ee9..95ae8f4 100644 --- a/k8s/prole/prole-db-recovery.yaml.tpl +++ b/k8s/prole/prole-db-recovery.yaml.tpl @@ -5,7 +5,7 @@ metadata: name: prole-db spec: instances: 3 - imageName: k3d-prole-registry:5000/prole-db:18-070 + imageName: k3d-prole-registry:5000/prole-db:18-089 postgresUID: 100 postgresGID: 101 maxSyncReplicas: 1 diff --git a/k8s/prole/prole-db.yaml b/k8s/prole/prole-db.yaml index 84ca595..918e791 100644 --- a/k8s/prole/prole-db.yaml +++ b/k8s/prole/prole-db.yaml @@ -4,7 +4,7 @@ metadata: name: prole-db spec: instances: 3 - imageName: k3d-prole-registry:5000/prole-db:18-070 + imageName: k3d-prole-registry:5000/prole-db:18-089 postgresUID: 100 postgresGID: 101 maxSyncReplicas: 1 diff --git a/mock_val/common_core_lib.sh b/mock_val/common_core_lib.sh new file mode 100644 index 0000000..1cf5b63 --- /dev/null +++ b/mock_val/common_core_lib.sh @@ -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 +} diff --git a/mock_val/etc/home.sh b/mock_val/etc/home.sh new file mode 100755 index 0000000..df180ed --- /dev/null +++ b/mock_val/etc/home.sh @@ -0,0 +1,2 @@ +#!/bin/bash +echo hometest diff --git a/mock_val/home.sh b/mock_val/home.sh new file mode 100755 index 0000000..df180ed --- /dev/null +++ b/mock_val/home.sh @@ -0,0 +1,2 @@ +#!/bin/bash +echo hometest diff --git a/mock_val/init_argocd.sh b/mock_val/init_argocd.sh new file mode 100755 index 0000000..d715e66 --- /dev/null +++ b/mock_val/init_argocd.sh @@ -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 diff --git a/mock_val/init_certmgr.sh b/mock_val/init_certmgr.sh new file mode 100755 index 0000000..7e7c047 --- /dev/null +++ b/mock_val/init_certmgr.sh @@ -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 </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 diff --git a/mock_val/init_cloudnative_pg.sh b/mock_val/init_cloudnative_pg.sh index 1537670..db3148c 100755 --- a/mock_val/init_cloudnative_pg.sh +++ b/mock_val/init_cloudnative_pg.sh @@ -204,8 +204,8 @@ openbao_url() { if curl -sS "http://127.0.0.1:8200/v1/sys/health" >/dev/null 2>&1; then echo "http://127.0.0.1:8200" return 0 - elif curl -sS "http://127.0.0.1:18200/v1/sys/health" >/dev/null 2>&1; then - echo "http://127.0.0.1:18200" + elif curl -sS "http://127.0.0.1:8200/v1/sys/health" >/dev/null 2>&1; then + echo "http://127.0.0.1:8200" return 0 else echo "" @@ -577,6 +577,137 @@ apply_prole_manifest_file() { 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() { echo "Applying CloudNative-PG cluster and related resources ..." local image_override="${CNPG_IMAGE:-${PROLE_DB_IMAGE:-}}" @@ -726,6 +857,7 @@ wait_for_cnpg_pods() { echo " Timeout: ${timeout}s" local last_feedback=0 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 local now elapsed 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) 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 echo "ERROR: Some CNPG pods are in Error or CrashLoopBackOff state:" >&2 echo "$pods" | grep -E "Error|CrashLoopBackOff" >&2 return 1 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) local running_pods 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" return 0 fi - if curl -sS "http://127.0.0.1:18200/v1/sys/health" >/dev/null 2>&1; then - echo "http://127.0.0.1:18200" + if curl -sS "http://127.0.0.1:8200/v1/sys/health" >/dev/null 2>&1; then + echo "http://127.0.0.1:8200" return 0 fi if curl -sS "http://127.0.0.1:8200/v1/sys/health" >/dev/null 2>&1; then @@ -1432,6 +1598,11 @@ initialize() { fi 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 if ! wait_for_cnpg_pods 300; then @@ -1486,6 +1657,7 @@ deploy_cluster() { generate_tls_if_missing echo "Deploying $image to cluster $CNPG_CLUSTER_NAME in namespace $NAMESPACE..." + _ensure_prole_db_image || true apply_cnpg_cluster_manifest "$CNPG_MANIFEST" local current_image @@ -1503,6 +1675,20 @@ case "$ACTION" in recreate) ensure_tools "$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" ;; create) @@ -1513,7 +1699,20 @@ case "$ACTION" in delete) ensure_tools 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) ensure_tools diff --git a/mock_val/init_common_core.sh b/mock_val/init_common_core.sh new file mode 100755 index 0000000..efc9635 --- /dev/null +++ b/mock_val/init_common_core.sh @@ -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 diff --git a/mock_val/init_common_services.sh b/mock_val/init_common_services.sh index b39949a..03f3799 100755 --- a/mock_val/init_common_services.sh +++ b/mock_val/init_common_services.sh @@ -2,14 +2,30 @@ set -u 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" usage() { cat < +Usage: init_common_services.sh [-n|--namespace NS] [-k|--kerberos] -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. EOF } @@ -20,6 +36,10 @@ ENABLE_KERBEROS=0 while [ $# -gt 0 ]; do case "$1" in + -c|--config) + shift + shift + ;; -m|--mode) shift prole_set_mode "${1:-}" @@ -42,8 +62,9 @@ while [ $# -gt 0 ]; do ENABLE_KERBEROS=1 shift ;; - update|start|status) + update|start|status|verify|repair|reload|restart) ACTION="$1" + [[ "$ACTION" == "repair" ]] && ACTION="update" shift ;; -h|--help) @@ -69,6 +90,41 @@ REGISTRY_NS="${REGISTRY_NAMESPACE:-default}" 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 # --------------------------------------------------------------------------- @@ -91,6 +147,80 @@ _resolve_prole_data() { 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() { local prole_data prole_data=$(_resolve_prole_data) @@ -118,54 +248,186 @@ _preload_docker_images() { return 0 fi local cluster_name="${K3D_CLUSTER_NAME:-prole-dev-cluster}" - local tar_file - while IFS= read -r tar_file; do - echo " Importing $(basename "$tar_file") into k3d cluster '$cluster_name' ..." - if k3d image import "$tar_file" -c "$cluster_name" >/dev/null 2>&1; then - echo " [OK] $(basename "$tar_file")" - else - echo " [WARN] Failed to import $(basename "$tar_file"); image will be pulled at deploy time." >&2 - fi + + # ── Step 1: build indexed arrays of tar paths + resolved image names ── + local -a tar_paths=() + 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" + 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&2 + local _idx + for (( _i=0; _i&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) - # For k3s running from macOS: load tarballs into Docker, then push to - # the in-cluster registry:2. Falls back to Ansible ctr import if the - # registry push fails. - local registry="${LOCAL_REGISTRY:-}" - if [[ -z "$registry" ]]; then - # Derive from k3s server URL - local _srv="${PROLE_K3S_SERVER:-}" - if [[ -n "$_srv" ]]; then - _srv="${_srv#https://}"; _srv="${_srv#http://}"; _srv="${_srv%%/*}"; _srv="${_srv%%:*}" - [[ -n "$_srv" ]] && registry="${_srv}:5000" - fi - fi - local tar_file - while IFS= read -r tar_file; do - local base_name - base_name=$(basename "$tar_file" .tar) - echo " Loading $(basename "$tar_file") into local Docker ..." - local loaded_image="" - loaded_image=$(docker load -i "$tar_file" 2>/dev/null | sed -n 's/^Loaded image: //p' || true) - if [[ -z "$loaded_image" ]]; then - # Try to derive image name from tarball filename (safe_name convention) - loaded_image="${base_name//_//}" - loaded_image="${loaded_image%/*}:${loaded_image##*/}" - fi - 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 + # For k3s: copy tarballs directly to k3s nodes and import via + # `k3s ctr images import`. This avoids the slow docker-push-to-registry + # flow that can stall for minutes per image layer. + local playbook_dir="$SCRIPT_DIR/../infrastructure/playbooks" + local playbook="$playbook_dir/k3s_import_images.yml" + + if [[ -f "$playbook" ]]; then + # Build JSON array of objects for Ansible extra-vars + local images_json="[" + local first=1 + local tar_file + while IFS= read -r tar_file; do + [[ -z "$tar_file" ]] && continue + + # Extract image name from tarball manifest on host + local img_name="" + if command -v python3 >/dev/null 2>&1; then + img_name=$(python3 -c "import json, tarfile; +try: + with tarfile.open('$tar_file') as tar: + m = json.load(tar.extractfile('manifest.json')) + print(m[0]['RepoTags'][0]) +except Exception: + pass" 2>/dev/null || true) fi - echo " [WARN] Registry push failed; image will be pulled at deploy time." >&2 - else - echo " [WARN] No registry configured; image will be pulled at deploy time." >&2 + + # Fallback to safe_name derivation if python failed or not available + 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 - 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 @@ -176,7 +438,7 @@ _preload_docker_images() { } case "$ACTION" in - start|update|reload|initialize|restart) + start|update|reload|initialize|restart|repair) _preload_docker_images ;; esac @@ -190,12 +452,9 @@ if [ -x "$SCRIPT_DIR/init_service_layer.sh" ]; then exit $? 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} +OPENBAO_NAME=${OPENBAO_NAME:-openbao} ARGOCD_SERVER_NAME=${ARGOCD_SERVER_NAME:-argocd-server} GARAGE_NAME=${GARAGE_NAME:-garage} 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" secret "$GARAGE_SECRET_NAME" --ignore-not-found >/dev/null 2>&1 || true 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)" @@ -263,7 +533,7 @@ echo "Deploying common services (namespace=$NS, action=$ACTION)" rc=0 case "$ACTION" in - start|update|reload|initialize|restart) + start|update|reload|initialize|restart|status) migrate_common_services ;; esac @@ -274,6 +544,13 @@ else echo "WARN: init_opentofu.sh not found; skipping OpenTofu." 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 ARGOCD_NAMESPACE="$ARGOCD_NS" REGISTRY_NAMESPACE="$REGISTRY_NS" \ "$SCRIPT_DIR/init_registry.sh" -n "$ARGOCD_NS" --registry-namespace "$REGISTRY_NS" "$ACTION" || rc=$? diff --git a/mock_val/init_database_options.sh b/mock_val/init_database_options.sh new file mode 100755 index 0000000..764b452 --- /dev/null +++ b/mock_val/init_database_options.sh @@ -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 < "$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" diff --git a/mock_val/init_garage_store.sh b/mock_val/init_garage_store.sh index fee4a70..ef71ed9 100755 --- a/mock_val/init_garage_store.sh +++ b/mock_val/init_garage_store.sh @@ -10,38 +10,44 @@ set -euo pipefail # Initialize SCRIPT_DIR 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 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 +set -- "${COMMON_CORE_ARGS[@]}" +common_core_parse_args "$@" + +if [[ "${COMMON_CORE_HELP:-0}" == 1 ]]; then + common_core_usage "$0" + exit 0 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 mkdir -p "$(dirname "$GARAGE_INIT_LOG")" exec > >(tee -a "$GARAGE_INIT_LOG") 2>&1 fi +RESOLVED_NAMESPACE="$(common_core_resolve_namespace "default")" +common_core_apply_namespace "$RESOLVED_NAMESPACE" + GARAGE_NAME=${GARAGE_NAME:-garage} GARAGE_SECRET_NAME=${GARAGE_SECRET_NAME:-garage-secrets} GARAGE_NODE_CAPACITY=${GARAGE_NODE_CAPACITY:-10GB} GARAGE_ZONE=${GARAGE_ZONE:-local} -NAMESPACE=${NAMESPACE:-default} -SERVICE_NAMESPACE=${SERVICE_NAMESPACE:-} -if [[ -z "${GARAGE_NAMESPACE:-}" ]]; then - if [[ -n "$SERVICE_NAMESPACE" ]]; then - GARAGE_NAMESPACE="$SERVICE_NAMESPACE" - else - GARAGE_NAMESPACE="$NAMESPACE" - fi -fi +GARAGE_NAMESPACE=${GARAGE_NAMESPACE:-$RESOLVED_NAMESPACE} NAMESPACE="$GARAGE_NAMESPACE" # Support both PROLE_HOME/k8s and sibling k8s directory @@ -72,19 +78,6 @@ GARAGE_APPLY_CHANGED=0 GARAGE_CONFIG_CHANGED=0 GARAGE_STATEFULSET_CHANGED=0 -usage() { - cat </dev/null || { echo "Missing required tool: $t" >&2; exit 1; } @@ -384,7 +377,7 @@ status() { } case "$ACTION" in - start) + start|initialize|update|reload|restart) ensure_tools ensure_namespace ensure_secrets @@ -403,24 +396,11 @@ case "$ACTION" in echo "Deleting Garage manifests from namespace '$NAMESPACE'..." 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 ;; *) - usage + common_core_usage "$0" + exit 1 ;; esac diff --git a/mock_val/init_gitea.sh b/mock_val/init_gitea.sh new file mode 100755 index 0000000..861c6e5 --- /dev/null +++ b/mock_val/init_gitea.sh @@ -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 < Deployment mode (default: ${MODE:-k3d}) + -n, --namespace Target namespace (default: gitea) + -c, --config 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" </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" <>"$MANIFEST_FILE" <&2; } @@ -237,8 +237,8 @@ openbao_url() { if curl -sS "http://127.0.0.1:8200/v1/sys/health" >/dev/null 2>&1; then echo "http://127.0.0.1:8200" return 0 - elif curl -sS "http://127.0.0.1:18200/v1/sys/health" >/dev/null 2>&1; then - echo "http://127.0.0.1:18200" + elif curl -sS "http://127.0.0.1:8200/v1/sys/health" >/dev/null 2>&1; then + echo "http://127.0.0.1:8200" return 0 else echo "" diff --git a/mock_val/init_kerberos_test.sh b/mock_val/init_kerberos_test.sh index 14c244b..faecd7a 100755 --- a/mock_val/init_kerberos_test.sh +++ b/mock_val/init_kerberos_test.sh @@ -287,8 +287,8 @@ openbao_url() { if curl -sS "http://127.0.0.1:8200/v1/sys/health" >/dev/null 2>&1; then echo "http://127.0.0.1:8200" return 0 - elif curl -sS "http://127.0.0.1:18200/v1/sys/health" >/dev/null 2>&1; then - echo "http://127.0.0.1:18200" + elif curl -sS "http://127.0.0.1:8200/v1/sys/health" >/dev/null 2>&1; then + echo "http://127.0.0.1:8200" return 0 else echo "" diff --git a/mock_val/init_kong.sh b/mock_val/init_kong.sh index f56dd3c..593865f 100755 --- a/mock_val/init_kong.sh +++ b/mock_val/init_kong.sh @@ -1,66 +1,65 @@ #!/usr/bin/env bash + set -euo pipefail + # init_kong.sh # 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 # - Routes /backup/* to prole-db-manager # - Creates the kong declarative config as a ConfigMap # - Applies the kong deployment and service manifests # - Provides start/stop/status/restart actions -# -# Usage: -# ./init_kong.sh [--mode MODE] [-n NAMESPACE] 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" -if [[ "${1:-}" == "--mode" || "${1:-}" == "-m" ]]; then - prole_set_mode "${2:-}" - shift 2 -elif [[ "${1:-}" == --mode=* || "${1:-}" == -m=* ]]; then - prole_set_mode "${1#*=}" - shift +set -- "${COMMON_CORE_ARGS[@]}" +common_core_parse_args "$@" + +if [[ -z "${PROLE_MODE:-}" ]]; then + export PROLE_MODE="k3s" fi -NAMESPACE="${NAMESPACE:-default}" +if [[ "${COMMON_CORE_HELP:-0}" == 1 ]]; then + common_core_usage "$0" + exit 0 +fi -while [[ $# -gt 0 ]]; do - case "$1" in - -n|--namespace) - shift - if [[ -z "${1:-}" ]]; then - echo "ERROR: -n/--namespace requires a value" >&2 - exit 2 - fi - NAMESPACE="$1" - ;; - -n=*|--namespace=*) - NAMESPACE="${1#*=}" - ;; - start|stop|status|restart) - ACTION="$1" - ;; - -h|--help) - usage - ;; - *) - echo "ERROR: Unknown argument: $1" >&2 - exit 2 - ;; - esac - shift -done +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" -ACTION="${ACTION:-start}" PROLE_HOME=${PROLE_HOME:-$(cd "$SCRIPT_DIR/.." && pwd)} 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_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 DB_MANAGER_SERVICE="${DB_MANAGER_SERVICE:-prole-db-manager}" @@ -68,13 +67,14 @@ DB_MANAGER_PORT="${DB_MANAGER_PORT:-80}" usage() { cat < +Usage: $0 [--mode MODE] [-n NAMESPACE] [${COMMON_CORE_ACTIONS//|/|}] [-c conf/prole.cfg] 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 status Show Kong pod/service status restart Restart Kong pods + update Create or update Kong resources USAGE exit 1 } @@ -96,7 +96,7 @@ create_kong_config() { echo "Creating/updating Kong declarative config '$KONG_CONFIG_NAME' in namespace '$NAMESPACE' ..." local kong_yml - kong_yml=$(cat </dev/null 2>&1; then echo "http://127.0.0.1:8200" return 0 - elif curl -sS "http://127.0.0.1:18200/v1/sys/health" >/dev/null 2>&1; then - echo "http://127.0.0.1:18200" + elif curl -sS "http://127.0.0.1:8200/v1/sys/health" >/dev/null 2>&1; then + echo "http://127.0.0.1:8200" return 0 else echo "" diff --git a/mock_val/init_openbao.sh b/mock_val/init_openbao.sh index ac72d48..5359f75 100755 --- a/mock_val/init_openbao.sh +++ b/mock_val/init_openbao.sh @@ -11,22 +11,65 @@ set -euo pipefail # Initialize SCRIPT_DIR 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 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 echo "ERROR: PROLE_SERVICE is not defined. Provide PROLE_HOME/env.sh or ~/.prole/env.sh" >&2 exit 1 fi -ACTION="" -NAMESPACE_OVERRIDE="" - # Defaults OPENBAO_NAME=${OPENBAO_NAME:-openbao} 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 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" 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_PATH_NAMESPACE=${OPENBAO_PATH_NAMESPACE:-${NAMESPACE:-default}} OPENBAO_RESOURCE_NAMESPACE="$OPENBAO_NAMESPACE" @@ -227,6 +221,21 @@ bao_service_url() { echo "$PROLE_OPENBAO_URL" return 0 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}" } @@ -234,7 +243,44 @@ bao_service_url() { _OPENBAO_PF_PID="" _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 </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_STARTED=1 @@ -268,9 +314,11 @@ stop_openbao_port_forward() { } wait_for_openbao_http() { - local svc - svc="$(bao_service_url)" local max_retries=${1:-20} + local svc="${2:-}" + if [[ -z "$svc" ]]; then + svc="$(bao_service_url)" + fi local count=0 while ! curl -sS "$svc/v1/sys/health" >/dev/null 2>&1; do if [[ $count -ge $max_retries ]]; then @@ -434,11 +482,16 @@ apply_openbao_host_network_patch() { } wait_for_openbao() { - echo "Waiting for OpenBao to become ready ..." - if kubectl get statefulset/$OPENBAO_NAME -n "$OPENBAO_RESOURCE_NAMESPACE" >/dev/null 2>&1; then - kubectl rollout status statefulset/$OPENBAO_NAME -n "$OPENBAO_RESOURCE_NAMESPACE" --timeout=${ROLLOUT_TIMEOUT:-300s} + local wait_ns="$OPENBAO_RESOURCE_NAMESPACE" + if should_skip_deployment; then + 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 - 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 } @@ -446,8 +499,10 @@ init_openbao_kv_and_store_admin_key() { local token token=$(cat "$root_token_file") # Enable kv (if not already) and write keys - local svc - svc="$(bao_service_url)" + local svc="${1:-}" + if [[ -z "$svc" ]]; then + svc="$(bao_service_url)" + fi echo "Configuring KV at $svc ..." # 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' cmd_status() { - echo "--- init_primary_domain status ---" + echo "--- openbao status ---" echo "Namespace: $OPENBAO_RESOURCE_NAMESPACE" echo "Secret namespace: $BAO_NAMESPACE" echo "OpenBao name: $OPENBAO_NAME" @@ -679,6 +734,22 @@ cmd_status() { # Prefer explicit override, then localhost port-forward, then cluster DNS local svc_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 echo "[OK] OpenBao service reachable" if [[ -s "$root_token_file" ]]; then @@ -696,13 +767,8 @@ cmd_status() { ok=1 fi - # Service certificate existence (CNPG TLS) - local CNPG_CLUSTER_NAME - 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" + if [[ "$pf_started" == "1" ]]; then + stop_openbao_port_forward fi echo "-------------------------------------" @@ -720,16 +786,23 @@ case "$ACTION" in ensure_tools ensure_k8s_tools ensure_namespace - generate_openbao_manifests - if should_apply_kerberos_configmap; then - generate_kerberos_configmap + if should_skip_deployment; then + expose_common_openbao + 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 - ensure_admin_keypair - k3d_cleanup_pending_openbao_pvc - apply_k8s - apply_openbao_host_network_patch 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) ensure_tools @@ -759,13 +832,17 @@ case "$ACTION" in ensure_tools ensure_k8s_tools ensure_namespace - generate_openbao_manifests - if should_apply_kerberos_configmap; then - generate_kerberos_configmap + if should_skip_deployment; then + expose_common_openbao + 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 - k3d_cleanup_pending_openbao_pvc - apply_k8s - apply_openbao_host_network_patch wait_for_openbao # Read password from stdin if provided 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 exit 1 fi - wait_for_openbao_http 20 - init_openbao_kv_and_store_admin_key - prole_register_port_forward "openbao" "${OPENBAO_NAMESPACE:-default}" "svc/openbao" "8200" "8200" "127.0.0.1" "TCP" "OpenBao" + + # After port-forward, always use the local URL for initialization so we + # 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)" ;; update|reload) ensure_tools ensure_k8s_tools ensure_namespace - generate_openbao_manifests - if should_apply_kerberos_configmap; then - generate_kerberos_configmap + if should_skip_deployment; then + expose_common_openbao + 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 - k3d_cleanup_pending_openbao_pvc - apply_k8s - apply_openbao_host_network_patch - 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" echo "Re-applied manifests." ;; *) - echo "Usage: $0 [-n|--namespace NAME] {start|stop|status|restart|initialize|update|reload}" >&2 + common_core_usage "$0" exit 2 ;; esac diff --git a/mock_val/init_opentofu.sh b/mock_val/init_opentofu.sh index e7b4619..91ec0f4 100755 --- a/mock_val/init_opentofu.sh +++ b/mock_val/init_opentofu.sh @@ -9,18 +9,38 @@ set -euo pipefail 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 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 echo "ERROR: PROLE_SERVICE is not defined. Provide PROLE_HOME/env.sh or ~/.prole/env.sh" >&2 exit 1 fi -ACTION="" -NAMESPACE_OVERRIDE="" - OPENTOFU_NAME=${OPENTOFU_NAME:-opentofu} OPENTOFU_ADMIN_USER=${OPENTOFU_ADMIN_USER:-admin} @@ -32,55 +52,6 @@ else OPENTOFU_MANIFEST_DIR="$SCRIPT_DIR/../k8s/opentofu" 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_SECRET_NAMESPACE=${OPENTOFU_SECRET_NAMESPACE:-${NAMESPACE:-default}} 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 echo "http://127.0.0.1:8200" return 0 - elif curl -sS "http://127.0.0.1:18200/v1/sys/health" >/dev/null 2>&1; then - echo "http://127.0.0.1:18200" + elif curl -sS "http://127.0.0.1:8200/v1/sys/health" >/dev/null 2>&1; then + echo "http://127.0.0.1:8200" return 0 else echo "" @@ -228,7 +199,7 @@ case "${ACTION:-}" in 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 ;; esac diff --git a/mock_val/init_registry.sh b/mock_val/init_registry.sh index e3f1303..4b8fce8 100755 --- a/mock_val/init_registry.sh +++ b/mock_val/init_registry.sh @@ -9,6 +9,22 @@ set -euo pipefail 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 # shellcheck disable=SC1090 source "$SCRIPT_DIR/prole_cfg.sh" @@ -54,6 +70,15 @@ EOF while [[ $# -gt 0 ]]; do 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) shift prole_set_mode "${1:-}" diff --git a/mock_val/init_service_layer.sh b/mock_val/init_service_layer.sh index a9b2e10..75ff209 100755 --- a/mock_val/init_service_layer.sh +++ b/mock_val/init_service_layer.sh @@ -4,22 +4,30 @@ set -euo pipefail # init_service_layer.sh # 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 # - Migrate service layer to a new namespace 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" -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 ACTION="" @@ -36,6 +44,19 @@ EOF while [[ $# -gt 0 ]]; do case "$1" in + -c|--config) + shift + shift + ;; + -m|--mode) + shift + prole_set_mode "${1:-}" + shift + ;; + -m=*|--mode=*) + prole_set_mode "${1#*=}" + shift + ;; -n|--namespace) shift SERVICE_NAMESPACE_OVERRIDE="${1:-}" @@ -67,8 +88,9 @@ while [[ $# -gt 0 ]]; do TO_NAMESPACE="${1#*=}" shift ;; - start|update|restart|status|stop|migrate|initialize|reload) + start|update|restart|status|stop|migrate|initialize|reload|repair) ACTION="$1" + [[ "$ACTION" == "repair" ]] && ACTION="update" shift ;; -h|--help) @@ -144,7 +166,7 @@ deploy_service_layer() { ensure_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 start|initialize|update|reload) argocd_action="update" ;; restart) argocd_action="restart" ;; @@ -161,6 +183,14 @@ deploy_service_layer() { *) opentofu_action="update" ;; 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 start|initialize|update|reload) garage_action="start" ;; restart) garage_action="restart" ;; @@ -169,6 +199,14 @@ deploy_service_layer() { *) garage_action="start" ;; 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 stop) kdc_action="cleanup" ;; 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" \ "$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" \ "$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 SERVICE_NAMESPACE="$ns" PROLE_KDC_NAMESPACE="$ns" \ "$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 OPENTOFU_NAMESPACE="$ns" SERVICE_NAMESPACE="$ns" \ "$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" \ "$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 SERVICE_NAMESPACE="$ns" PROLE_KDC_NAMESPACE="$ns" \ "$SCRIPT_DIR/init_kdc.sh" cleanup || true diff --git a/mock_val/local_registry.log b/mock_val/local_registry.log new file mode 100644 index 0000000..21a7dce --- /dev/null +++ b/mock_val/local_registry.log @@ -0,0 +1,2 @@ + +# Registry disabled (mode=unknown) @ 2026-03-01 18:10:50 diff --git a/mock_val/prole-db-passwwd.sh b/mock_val/prole-db-passwwd.sh index 606d624..5053065 100755 --- a/mock_val/prole-db-passwwd.sh +++ b/mock_val/prole-db-passwwd.sh @@ -176,8 +176,8 @@ openbao_url() { printf '%s' "http://127.0.0.1:8200" return 0 fi - if curl -sS "http://127.0.0.1:18200/v1/sys/health" >/dev/null 2>&1; then - printf '%s' "http://127.0.0.1:18200" + if curl -sS "http://127.0.0.1:8200/v1/sys/health" >/dev/null 2>&1; then + printf '%s' "http://127.0.0.1:8200" return 0 fi return 1 diff --git a/mock_val/prole_cfg.sh b/mock_val/prole_cfg.sh index 2bba813..39ddf68 100644 --- a/mock_val/prole_cfg.sh +++ b/mock_val/prole_cfg.sh @@ -463,7 +463,7 @@ def resolve_openbao_ref(value: str) -> str: url = os.environ.get("PROLE_OPENBAO_URL") if not url: - for p in ["8200", "18200"]: + for p in ["8200", "8200"]: try: with urllib.request.urlopen(f"http://127.0.0.1:{p}/v1/sys/health", timeout=0.5) as r: if r.getcode() == 200: diff --git a/mock_val/secrets/openbao-root-token b/mock_val/secrets/openbao-root-token index 545ae71..93ca142 100644 --- a/mock_val/secrets/openbao-root-token +++ b/mock_val/secrets/openbao-root-token @@ -1 +1 @@ -405f3fd4ceea0f00952532333e3035b240b404e9d30e0b7f \ No newline at end of file +root \ No newline at end of file diff --git a/mock_val/status_common_services.sh b/mock_val/status_common_services.sh index a68b182..9089aab 100755 --- a/mock_val/status_common_services.sh +++ b/mock_val/status_common_services.sh @@ -2,6 +2,22 @@ set -u 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" @@ -9,7 +25,7 @@ usage() { cat </dev/null 2>&1; then echo "ERROR: kubectl not found in PATH" @@ -69,6 +108,8 @@ if [[ -n "${KUBECONFIG:-}" ]]; then fi echo "Namespace: $NS" echo "ArgoCD Namespace: $ARGOCD_NS" +echo "Kong Namespace: $KONG_NS" +echo "Cert-Manager Namespace: $CERTMGR_NS" echo "" run_cmd() { @@ -79,29 +120,37 @@ run_cmd() { echo "== Services ==" 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 - run_cmd kubectl -n "$NS" get svc opentofu garage auth -else - run_cmd kubectl -n "$NS" get svc opentofu garage + run_cmd kubectl -n "$NS" get svc auth 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 ==" 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 -if [[ "$ENABLE_KERBEROS" == "1" ]]; then - run_cmd kubectl -n "$NS" get deploy opentofu auth +run_cmd kubectl -n "$NS" get deploy opentofu +if kubectl -n "$NS" get statefulset openbao >/dev/null 2>&1; then + run_cmd kubectl -n "$NS" get statefulset openbao 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 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 ==" 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 - run_cmd kubectl -n "$NS" get pods | grep -Ei "opentofu|garage|auth" || true -else - run_cmd kubectl -n "$NS" get pods | grep -Ei "opentofu|garage" || true + run_cmd kubectl -n "$NS" get pods | grep -Ei "auth" || true 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 ==" missing=0 @@ -149,6 +198,9 @@ analyze_pods() { while IFS='|' read -r comp name ready status healthy; do [[ -z "$comp" ]] && continue + if [[ "$comp" == cert-manager* ]]; then + comp="certmgr" + fi COMP_NAMESPACE["$comp"]="$ns" if [[ "$healthy" == "1" ]]; then HEALTHY_COUNT["$comp"]=$(( ${HEALTHY_COUNT["$comp"]:-0} + 1 )) @@ -175,9 +227,13 @@ check_resource() { check_resource svc argocd-server "$ARGOCD_NS" check_resource svc opentofu "$NS" check_resource svc garage "$NS" +check_resource svc openbao "$NS" if [[ "$ENABLE_KERBEROS" == "1" ]]; then check_resource svc auth "$NS" 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-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-redis "$ARGOCD_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 check_resource deploy auth "$NS" fi check_resource statefulset argocd-application-controller "$ARGOCD_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 - pod_filter="opentofu|garage|auth" + pod_filter="opentofu|garage|openbao|auth|kong|cert-manager" fi 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 echo "" @@ -208,64 +275,257 @@ fi if [ $missing -gt 0 ]; then echo "" - echo "Status: FAILED ($missing resources missing)" - exit 1 + # ── Attempt to repair a completely absent cert-manager before failing ────── + 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 +# ── 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 - repairable=() - fail_all=() - for comp in argocd openbao opentofu garage auth; do - if [[ "$comp" == "auth" && "$ENABLE_KERBEROS" != "1" ]]; then - continue + + # ── Round 1: delete blocked pods ────────────────────────────────────────── + echo "" + echo "== Repair Round 1: replacing blocked pods ==" + 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 - hc=${HEALTHY_COUNT["$comp"]:-0} - bc=${BLOCKED_COUNT["$comp"]:-0} - if [[ "$bc" -gt 0 && "$hc" -gt 0 ]]; then - repairable+=("$comp") - elif [[ "$bc" -gt 0 && "$hc" -eq 0 ]]; then - fail_all+=("$comp") + done + echo " Waiting for pod replacement (15s)..." + sleep 15 + _recheck_pods + + 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 done - if [ ${#fail_all[@]} -gt 0 ]; then - echo "" - echo "Repair skipped: all pods blocked for ${fail_all[*]}." - echo "Action: Deploy Missing Services." - echo "" - echo "Status: NOTICE (blocked pods detected)" - exit 2 + if [[ -x "$SCRIPT_DIR/init_service_layer.sh" ]]; then + echo " [Round 3] Re-initializing service layer ..." + _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 - 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 "== Repair ==" - echo "Repairing blocked pods for: ${repairable[*]}" - 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 + echo "Status: OK (resolved in repair round 3)" + exit 0 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 echo "" diff --git a/prole-db/.version b/prole-db/.version index c147342..7730ef7 100644 --- a/prole-db/.version +++ b/prole-db/.version @@ -1 +1 @@ -81 \ No newline at end of file +89 \ No newline at end of file diff --git a/prole-db/Dockerfile b/prole-db/Dockerfile index 2ab079d..e0e25b3 100644 --- a/prole-db/Dockerfile +++ b/prole-db/Dockerfile @@ -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-pg-stat-monitor18; \ 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 \ percona-postgresql-contrib \ freetds-dev \ diff --git a/prole-db/Dockerfile.percona.template b/prole-db/Dockerfile.percona.template index 1d18db0..b638bb2 100644 --- a/prole-db/Dockerfile.percona.template +++ b/prole-db/Dockerfile.percona.template @@ -75,6 +75,7 @@ RUN set -eux; \ RUN set -eux; \ apt-get update; \ {{EXTENSION_INSTALL_STEPS}} + apt-get install -y --no-install-recommends percona-pg-tde{{MAJOR_VERSION}}; \ apt-get install -y --no-install-recommends \ percona-postgresql-contrib \ freetds-dev \ diff --git a/src/db-manager/.gitignore b/src/db-manager/.gitignore deleted file mode 100644 index c2658d7..0000000 --- a/src/db-manager/.gitignore +++ /dev/null @@ -1 +0,0 @@ -node_modules/ diff --git a/src/db-manager/Dockerfile b/src/db-manager/Dockerfile deleted file mode 100644 index b374f63..0000000 --- a/src/db-manager/Dockerfile +++ /dev/null @@ -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"] diff --git a/src/db-manager/package-lock.json b/src/db-manager/package-lock.json deleted file mode 100644 index c9607de..0000000 --- a/src/db-manager/package-lock.json +++ /dev/null @@ -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" - } - } - } -} diff --git a/src/db-manager/package.json b/src/db-manager/package.json deleted file mode 100644 index df3d823..0000000 --- a/src/db-manager/package.json +++ /dev/null @@ -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" - } -} diff --git a/src/db-manager/server.js b/src/db-manager/server.js deleted file mode 100644 index f3b8ccb..0000000 --- a/src/db-manager/server.js +++ /dev/null @@ -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}`); - }); -} diff --git a/src/db-manager/test/server.test.js b/src/db-manager/test/server.test.js deleted file mode 100644 index be5badd..0000000 --- a/src/db-manager/test/server.test.js +++ /dev/null @@ -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'); - } - }); - }); -}); diff --git a/tests/installer/conftest.py b/tests/installer/conftest.py new file mode 100644 index 0000000..f6ad340 --- /dev/null +++ b/tests/installer/conftest.py @@ -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 diff --git a/tests/installer/test_config_helpers.py b/tests/installer/test_config_helpers.py new file mode 100644 index 0000000..a8cb83b --- /dev/null +++ b/tests/installer/test_config_helpers.py @@ -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 diff --git a/tests/installer/test_controller.py b/tests/installer/test_controller.py new file mode 100644 index 0000000..da5450b --- /dev/null +++ b/tests/installer/test_controller.py @@ -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 diff --git a/tests/installer/test_deploy_helpers.py b/tests/installer/test_deploy_helpers.py new file mode 100644 index 0000000..64f5744 --- /dev/null +++ b/tests/installer/test_deploy_helpers.py @@ -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 diff --git a/tests/installer/test_milestones_extended.py b/tests/installer/test_milestones_extended.py new file mode 100644 index 0000000..6ecc8f8 --- /dev/null +++ b/tests/installer/test_milestones_extended.py @@ -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 diff --git a/tests/installer/test_monitor.py b/tests/installer/test_monitor.py new file mode 100644 index 0000000..3a3a8ab --- /dev/null +++ b/tests/installer/test_monitor.py @@ -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" diff --git a/tests/installer/test_screen.py b/tests/installer/test_screen.py index da43ac5..0e73bde 100644 --- a/tests/installer/test_screen.py +++ b/tests/installer/test_screen.py @@ -39,6 +39,13 @@ def test_render_title_no_canvas(): @patch("platform.system", return_value="Darwin") 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() try: console = TerminalConsole(root) @@ -51,3 +58,5 @@ def test_terminal_console(mock_platform): assert console.text.cget("state") == "disabled" finally: root.destroy() + + diff --git a/tests/installer/test_stream_exec.py b/tests/installer/test_stream_exec.py index 193d85b..2846c0a 100644 --- a/tests/installer/test_stream_exec.py +++ b/tests/installer/test_stream_exec.py @@ -37,6 +37,40 @@ def test_run_streaming_cmd_streams_stdout_incrementally(): 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(): stdout_chunks: list[str] = [] stderr_chunks: list[str] = [] diff --git a/tests/test_navigation.py b/tests/test_navigation.py index a46bbef..2cba429 100644 --- a/tests/test_navigation.py +++ b/tests/test_navigation.py @@ -105,6 +105,9 @@ def test_navigation_flow_standard(mock_installer): with patch.object( 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() 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): - # init_password -> database_options + # init_password -> init_db_build -> database_options 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() assert mock_installer.pages[mock_installer.page_index][0] == "database_options"