Add GitOps provider choice screen and cluster UI improvements

- Add _render_gitops_choice_page() to GitOpsScreenMixin for explicit
  Gitea / ArgoCD / None provider selection with radio buttons
- Extend cluster screen with GitOps provider radio group and canvas layout
- Refactor services screen layout and navigation registration
- Register new gitops choice screen in screens __init__ / navigation
- Expand Kong init scripts (etc/ and mock_val/) with additional logic
- Update init_cnpg_backup.sh with minor fix
- Refresh conf/service and conf/prod prole.cfg generated configs
- Update conf/port-mapping.cfg port entries
- Update Supabase Helm values.yaml
- Bump modes/k3s/knoe-db/.version
- Update network scan description
- Add/expand tests: test_cluster_screen_layout, test_gitops_choice_screen

Co-authored-by: Junie <junie@jetbrains.com>
This commit is contained in:
chrisfu 2026-03-24 22:12:25 -07:00
parent 43fd34ea87
commit a1177d71bc
16 changed files with 653 additions and 184 deletions

View File

@ -2,10 +2,6 @@
# Format: key: local=... remote=... ns=... svc=... address=...
argocd: local=8081 remote=80 ns=argocd svc=argocd-server address=0.0.0.0
supabase-studio: local=18080 remote=3000 ns=supabase svc=studio address=0.0.0.0
supabase-auth: local=9999 remote=9999 ns=supabase svc=auth address=127.0.0.1
supabase-rest: local=3001 remote=3000 ns=supabase svc=rest address=0.0.0.0
supabase-realtime: local=4000 remote=4000 ns=supabase svc=realtime address=0.0.0.0
garage: local=3900 remote=3900 ns=knoe-system svc=garage address=0.0.0.0
openbao: local=8200 remote=8200 ns=knoe-system svc=openbao address=0.0.0.0
opentofu: local=8080 remote=8080 ns=knoe-system svc=opentofu address=0.0.0.0
@ -13,4 +9,4 @@ dashboard: local=8443 remote=443 ns=kubernetes-dashboard svc=kubernetes-dashboar
postgres: local=5432 remote=5432 ns=knoe-db svc=knoe-db-rw address=0.0.0.0
prometheus: local=9090 remote=9090 ns=monitoring svc=kps-kube-prometheus-stack-prometheus address=127.0.0.1
grafana: local=3000 remote=80 ns=monitoring svc=kps-grafana address=0.0.0.0
supabase-kong: local=8000 remote=8000 ns=supabase svc=kong address=0.0.0.0
certmgr: local=9443 remote=9443 ns=cert-manager svc=cert-manager-webhook address=127.0.0.1

View File

@ -1,5 +1,5 @@
; Prole Master Configuration File
; Generated by install.py on 2026-03-21 23:40:10
; Generated by install.py on 2026-03-24 18:24:09
; This file is used as input for Ansible deployment and k8s cluster creation.
[User]
@ -38,7 +38,8 @@ dependencies.verify_all = false
disk_selection.disk_type = local
disk_selection.local_path = /Users/chrisfu/dev/prole/prole-tools-app/dist
disk_selection.removable_mount =
env_setup.NAMESPACE = ${NAMESPACE}
env_setup.CLUSTER_NAME = ${CLUSTER_NAME}
env_setup.DATABASE_NAMESPACE = ${DATABASE_NAMESPACE}
env_setup.PROLE_CONF = /Users/chrisfu/dev/prole/conf
env_setup.PROLE_DATA = /Users/chrisfu/dev/prole/data
env_setup.PROLE_HOME = /Users/chrisfu/dev/prole
@ -50,7 +51,7 @@ init_cluster.cluster_env = prod
init_cluster.deployment_target = prole-prod-cluster
init_cluster.gitops_enabled = false
init_cluster.k3s_server_url = https://myrddin.prole.org:6443
init_cluster.k3s_token = ${PROLE_SECRET:v1:iTRfvIPRo7v-aK5j:H6zki1L-02mHJQLXIjZyFnVWByy5TfoCASeGEi08tcfZCqqybxeCatiXK_NyADR1oElbaZnvjw83fuhircDNdRRY4sQo_hEuKU0ll9s9o6QZBKZZzWpgcU4WMX7YAS4aOgc8d5pBFUH0mC-ZcJ9lSWT3JxhgBXRdbVopQQ==}
init_cluster.k3s_token = ${PROLE_SECRET:v1:os8KqzMGFP1k3yBI:45ywbbblQIV-acyMkTt21Ff8g1H7zuZU2RXRInDQxLBsNxsqm7n18dU6CGCJ64GG7A9rblAZAaVVo9V7bMPq3JUED9z3vLXqnnDwx4JbyD5OnplIJ6KfF9PHLX-VMreioizAAcHN66gyhTc4R5Y3-z2q1aeJKtr4H4JqbQ==}
init_cluster.kerberos_enabled = true
init_cluster.mode = k8s
init_cluster.start_cluster = true
@ -58,8 +59,9 @@ 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.cluster_name = ${CLUSTER_NAME}
init_password.db_host_port = 5432
init_password.db_namespace = ${NAMESPACE}
init_password.db_namespace = ${DATABASE_NAMESPACE}
init_password.db_password =
init_password.db_password_confirm =
init_password.db_username = root
@ -67,7 +69,7 @@ init_password.generate_ssh_key = true
init_scripts.run_scripts = true
kerberos_config.enabled = true
kerberos_config.kdc = 10.0.0.3
kerberos_config.password = ${PROLE_SECRET:v1:rEiGEX6WGmmQ2vyE:EjwlefHMKjZwZRVIrh3x4VjqxnEddJ-EpRSaoqd3PjaZGTh5}
kerberos_config.password = ${PROLE_SECRET:v1:Ed93pzGEE14H4z1U:2V1lCQ4-UPJcU4StFDkixNY6jvbezCI4CAfwKbJyBwnOuGH1}
kerberos_config.realm = PROLE.ORG
kerberos_config.test_connection = false
kerberos_config.user = administrator
@ -79,17 +81,17 @@ ollama_config.server_port = 11434
[Global]
; Variables used by name in more than one place or assumed global scope
CLUSTER_ENV = prod
CLUSTER_NAME = knoe-db
DATABASE_NAMESPACE = knoe-db-17-7-043-18-140
DB_HOST_PORT = 5432
DB_PASSWORD =
DEPLOYMENT_MODE = k8s
DEPLOYMENT_TARGET = prole-prod-cluster
DOCKER_PRELOAD = false
NAMESPACE = knoe-db
OPTIONAL_WORKLOADS_MIN_READY_SCHEDULABLE_NODES = 2
KNOE_DB_USER = root
PROLE_HOME = $HOME/dev/prole
OPTIONAL_WORKLOADS_MIN_READY_SCHEDULABLE_NODES = 2
PROLE_K3S_SERVER = https://myrddin.prole.org:6443
PROLE_K3S_TOKEN = ${PROLE_SECRET:v1:cWc4c9mYxi20rxbR:wTCIMGahxg8rxxGotDtMhobeDIzf1k_9thQti2je2znzRVZVqV4TibePL21oghQvLLBbkjzd5hwIlUGGqq7sOYkpIetik4swjmaQnJ4DHGT82RWX3PZuuCrPWLGb5NOs4Hqv61TKYru5C-ImJzrPyEq7vJDgBG3aita0HQ==}
PROLE_K3S_TOKEN = ${PROLE_SECRET:v1:TTjP4kMPgOzEndoI:YhRZHb0kebPDxVBCIY9z5OyAUV4X01LCmCxLrq6dUDI7J0WUSnI62aVXXMegfoLf04WtWAII8grOdhxuUBGbWXQ_P2qyWguNZk4J8ag9CbSPfSxUliKkbvwOFYH_qwJi-A7SA40dYzLYYWj_066MWqTgab1xFcFiwntNOQ==}
PROLE_OPENTOFU_URL = http://127.0.0.1:8080
SERVICE_NAMESPACE = knoe-system
@ -155,7 +157,7 @@ MODE = k3d
CLUSTER_ENV = prole-service-cluster
DISPLAY_NAME = prole-service-cluster
K3S_SERVER_URL = https://myrddin.prole.org:6443
K3S_TOKEN = ${PROLE_SECRET:v1:3DHF_algPRkZXK_o:hMRZr17b7GihxuRs-6NPhlcS9h-aIctsZih2SS6dPCqwZabpKbG2ziZBgLiwOAhWih-ofvR8qbaOWdmbXurTYvWAWoj5F0MzOa35klQ6ujO2A2B2Y_uHrnzHXwkwEVufl9g8mucOkSMZ6UiVkNK4jYowVZHNTXe7uhghCA==}
K3S_TOKEN = ${PROLE_SECRET:v1:AMWPPVWetr8qzDVw:NpiGkrREpI1PJXF-W0d4Ryfe3C1FQiibBNn49sRIm5fggf8dj00_OLYdG5GUdxDZVLWbk_wSn0BK2PET7VOaCNU-B2iLyvGOuFQ41nhyPHZSwmgEPOq7YCtF8Kjv4WyENTTqXaoZaJ_3oWnrls6AalVPk85BfYuvzvybkQ==}
MODE = k3s
PIPELINE_URL = http://127.0.0.1:8080

View File

@ -1,5 +1,5 @@
; Prole Master Configuration File
; Generated by install.py on 2026-03-24 14:56:35
; Generated by install.py on 2026-03-24 21:31:16
; This file is used as input for Ansible deployment and k8s cluster creation.
[User]
@ -51,26 +51,25 @@ init_cluster.cluster_env = service
init_cluster.deployment_target = prole-service-cluster
init_cluster.gitops_enabled = false
init_cluster.k3s_server_url = https://myrddin.prole.org:6443
init_cluster.k3s_token = ${PROLE_SECRET:v1:8FFZzwnMNamN9Zl6:0ITXXU0Id4wJwpDLpxjHP7Yqi-2_W5Orzc0Imi7QB6tGUENG50fegqVvixwedBplvkBDladIb-gE2sI5MJH2p1xwRbMkb2kvBIItgYW6rBevMTkpRMV36aJshsNw8l74VoFD3LR5KUBV3f2AmLfXQTlfyGkkV64kOYgGDQ==}
init_cluster.k3s_token = ${PROLE_SECRET:v1:CWAF-McNyYMXWLPJ:c-bbfiF4rpG4oMLHfwcCWwxaHIYEmAY7fIgCQSy4at6g6qslSSrQhCDscFmNiZam-d9JqZAxJF1xqW8EAjzzQOoB04Iv_wCMUyZovWuzN9aLnV2RHWdr4jFA963NL6OrZ37KdFAXKJ2L_FPvLH_A9SmmWHFOy0Va0JLLuA==}
init_cluster.kerberos_enabled = true
init_cluster.mode = k3s
init_cluster.start_cluster = true
init_cluster.supabase_enabled = 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.cluster_name = ${CLUSTER_NAME}
init_password.db_host_port = 5432
init_password.db_namespace = ${DATABASE_NAMESPACE}
init_password.db_password = ${PROLE_SECRET:v1:I4c69IhpfCLlMdYM:GOPT9cox83ALUWacXGfZvEwMsEX8OMwT}
init_password.db_password_confirm = ${PROLE_SECRET:v1:I4c69IhpfCLlMdYM:GOPT9cox83ALUWacXGfZvEwMsEX8OMwT}
init_password.db_password = ${PROLE_SECRET:v1:ivQufWO7XUiAW8HJ:JpbStYbwhWw1VcPykbTuAe_-kKIjqmPh}
init_password.db_password_confirm = ${PROLE_SECRET:v1:ivQufWO7XUiAW8HJ:JpbStYbwhWw1VcPykbTuAe_-kKIjqmPh}
init_password.db_username = root
init_password.generate_ssh_key = true
init_scripts.run_scripts = true
kerberos_config.enabled = true
kerberos_config.init_authority = false
kerberos_config.kdc = 10.0.0.3
kerberos_config.password = ${PROLE_SECRET:v1:_m0-5CRSInOmZKpG:QfB3PmArZOX_S_pvr_vPHZMicTQ5H3yaVATDPW2D2aHGt-gF}
kerberos_config.password = ${PROLE_SECRET:v1:i3-4lYEoA2eq03rQ:c_zSY9qIrxxeaTr1X_G2l1pdJJDDnLvx7Tgat3mr4ihPsFLT}
kerberos_config.realm = PROLE.ORG
kerberos_config.test_connection = false
kerberos_config.user = administrator
@ -81,42 +80,30 @@ ollama_config.server_port = 11434
[Global]
; Variables used by name in more than one place or assumed global scope
ARGOCD_NAMESPACE = argocd
CLUSTER_ENV = service
CLUSTER_NAME = knoe-db
CNPG_ELIGIBLE_NODES = merlin.prole.org,myrddin.prole.org
CNPG_ELIGIBLE_NODES = merlin.prole.org,myrddin.prole.org,pi.prole.org
CNPG_STAGE1_NODE = merlin.prole.org
DATABASE_NAMESPACE = knoe-db
DB_HOST_PORT = 5432
DB_PASSWORD = ${PROLE_SECRET:v1:wy6mYFXcdTTosLHd:ARnY3pxB65ii9IO5Oh_kzO4xR6e1R6R_}
DB_PASSWORD = ${PROLE_SECRET:v1:gFx4tAqM_UUWuFeK:dT2GGXubo_bC5mHupS7PSYeRg3N5pFTG}
DEPLOYMENT_MODE = k3s
DEPLOYMENT_TARGET = prole-service-cluster
DOCKER_IMPORT_DIR =
DOCKER_PRELOAD = false
K3S_SERVER = https://myrddin.prole.org:6443
K3S_TOKEN = ${PROLE_SECRET:v1:-zIZOM5gyWjEhHPP:x0jUffEsPUfHkVHuvPpf2U9TCXx05CY5UKImE2MxCet6JXGxwPmnbbVXYVfWPc4NaUSIATBcxB7A7SFKmwzUdfyMWI43Ham3jQo_R5yyXIR-qPuoc7JJ85xFMPylbrfDB-kV5fVEFP6867UUKV12D8i7OLM6JUZDc0Nukw==}
KNOE_DB_USER = root
KUBECONTEXT = prole-k3s
OPENTOFU_URL = http://myrddin.prole.org:8080
OPTIONAL_WORKLOADS_MIN_READY_SCHEDULABLE_NODES = 2
PROLE_HOME = /Users/chrisfu/dev/prole
PROLE_K3S_SERVER = https://myrddin.prole.org:6443
PROLE_K3S_TOKEN = K107c8c6000488eca4a067d8a73119bbae2f07b4ea1bac7d8d3dc9c500cbb8acb18::server:04572345810eae2f9619a6ed4239702b
PROLE_K3S_TOKEN = ${PROLE_SECRET:v1:T_rh15yn36yLioXG:m_McT35zO06S4r-n6hf9iivldC6i4PUkSY1cxJW7jHC1A11kAITL1qZpyKmbMjXSjEo4KbU58eV_NrLQVjTQqy2-EUoyXhagbclA8XVnmn9ZtR6wIdDkKmqSzmygNweO_Zy68SgnsIqUCMVrIFY66h4K-kbCW3mKaQwFPw==}
PROLE_OPENTOFU_URL = http://127.0.0.1:8080
REGISTRY_NAMESPACE = knoe-system
SERVICE_NAMESPACE = knoe-system
SUPABASE_ADDITIONAL_REDIRECT_URLS = svc.prole.org
SUPABASE_PRIMARY_NODE = merlin.prole.org
SUPABASE_PV_BASE_DIR = /synology/d002/supabase
SUPABASE_PV_NODE = merlin.prole.org
SUPABASE_STORAGE_CLASS = merlin-local-iscsi-d002
SYNOLOGY_ROOTS = /synology/d001,/synology/d002,/synology/d003,/synology/d004
[Welcome]
; No configuration values captured yet for this section.
[Dependencies]
STATUS = All installed
; No configuration values captured yet for this section.
[Network]
AD_DC_HOST = myrddin.prole.org
@ -131,19 +118,7 @@ KDC_AUTO_DETECTED = 10.0.0.3
KERBEROS_AUTO_ENABLED = True
[Port Forwards]
PORT_FORWARD_K3S_MAPPING_1 = id=argocd;namespace=argocd;target=svc/argocd-server;address=0.0.0.0;hostPort=8081;servicePort=80;protocol=TCP;description=ArgoCD
PORT_FORWARD_K3S_MAPPING_10 = id=supabase-studio;namespace=supabase;target=svc/studio;address=0.0.0.0;hostPort=18080;servicePort=3000;protocol=TCP;description=Supabase Studio
PORT_FORWARD_K3S_MAPPING_11 = id=supabase-auth;namespace=supabase;target=svc/auth;address=127.0.0.1;hostPort=9999;servicePort=9999;protocol=TCP;description=Supabase Auth (GoTrue)
PORT_FORWARD_K3S_MAPPING_12 = 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_K3S_MAPPING_13 = id=supabase-realtime;namespace=supabase;target=svc/realtime;address=0.0.0.0;hostPort=4000;servicePort=4000;protocol=TCP;description=Supabase Realtime
PORT_FORWARD_K3S_MAPPING_2 = id=garage;namespace=knoe-system;target=svc/garage;address=0.0.0.0;hostPort=3900;servicePort=3900;protocol=TCP;description=Garage S3
PORT_FORWARD_K3S_MAPPING_3 = id=openbao;namespace=knoe-system;target=svc/openbao;address=0.0.0.0;hostPort=8200;servicePort=8200;protocol=TCP;description=OpenBao
PORT_FORWARD_K3S_MAPPING_4 = id=opentofu;namespace=knoe-system;target=svc/opentofu;address=0.0.0.0;hostPort=8080;servicePort=8080;protocol=TCP;description=OpenTofu
PORT_FORWARD_K3S_MAPPING_5 = id=dashboard;namespace=kubernetes-dashboard;target=svc/kubernetes-dashboard-kong-proxy;address=127.0.0.1;hostPort=8443;servicePort=443;protocol=TCP;description=Kubernetes Dashboard
PORT_FORWARD_K3S_MAPPING_6 = id=postgres;namespace=${DATABASE_NAMESPACE};target=svc/knoe-db-rw;address=0.0.0.0;hostPort=5432;servicePort=5432;protocol=TCP;description=PostgreSQL (primary)
PORT_FORWARD_K3S_MAPPING_7 = id=prometheus;namespace=monitoring;target=svc/kps-kube-prometheus-stack-prometheus;address=127.0.0.1;hostPort=9090;servicePort=9090;protocol=TCP;description=Prometheus UI
PORT_FORWARD_K3S_MAPPING_8 = id=grafana;namespace=monitoring;target=svc/kps-grafana;address=0.0.0.0;hostPort=3000;servicePort=80;protocol=TCP;description=Grafana UI
PORT_FORWARD_K3S_MAPPING_9 = id=supabase-kong;namespace=supabase;target=svc/kong;address=0.0.0.0;hostPort=8000;servicePort=8000;protocol=TCP;description=Supabase API (Kong)
; No configuration values captured yet for this section.
[System Environment]
PROLE_CONF = /Users/chrisfu/dev/prole/conf
@ -156,7 +131,7 @@ PROLE_SERVICE = /Users/chrisfu/dev/prole/etc
; No configuration values captured yet for this section.
[Kerberos Authentication]
STATUS = Initialized
; No configuration values captured yet for this section.
[Ollama]
OLLAMA_HOST = http://fairyland.prole.org:11434
@ -165,52 +140,52 @@ OLLAMA_SERVER_HOST = fairyland.prole.org
OLLAMA_SERVER_PORT = 11434
[Optional Features]
AT_REST_ENCRYPTION_ENABLED = true
GITOPS_ENABLED = false
KERBEROS_ENABLED = true
SUPABASE_ENABLED = true
AT_REST_ENCRYPTION_ENABLED = True
GITOPS_ENABLED = False
KERBEROS_ENABLED = True
SUPABASE_ENABLED = False
[GitOps]
STATUS = Skipped
; No configuration values captured yet for this section.
[Database Creation]
DB_USER = root
; No configuration values captured yet for this section.
[Initialize Cluster]
ENVIRONMENT = service
K3S_SERVER_URL = https://myrddin.prole.org:6443
K3S_TOKEN = ${PROLE_SECRET:v1:WjD201Yr7MHE56cs:wackF_EDDde7KmJp2SwbMlJi5mLdoHATkoyolRkPVqvnOVVScriN55ZHOO0cec-RvMIFQzhkL_yZbKhNkN9IIfkRdnAeGPDVELtM-wEDv_YlymTsrpYL_u79VR-nX3gKTCfcAha44ZyoBzObKTShzUx8eLCJMeLKYoPEKg==}
K3S_TOKEN = ${PROLE_SECRET:v1:TiaxxeJo-nsA8u39:0m7rdp2olmPjOdoTUzLE39BRwDk9dW82BAuh1doGV6UiEt7nYpSzL7-VYS3SSIWbu4l5X5t7SR6bMCydstky7aFvS75Telo1NXraGRpUNWGuC7w7MHf48ZOjJ7Yr00EmdVuA1xfa2qqImHIcSeMCK1mcKwZtvYp_DqLqNw==}
[Dev Cluster (k3d)]
CLUSTER_ENV = k3d-knoe-dev-cluster
CLUSTER_ENV = dev
DISPLAY_NAME = knoe-dev-cluster
KUBECTL_CONTEXT = service
KUBECTL_CONTEXT = prole-k3s
MODE = k3d
[Service Cluster (k3s)]
CLUSTER_ENV = prole-service-cluster
DISPLAY_NAME = prole-service-cluster
K3S_SERVER_URL = https://myrddin.prole.org:6443
K3S_TOKEN = ${PROLE_SECRET:v1:ExAOsZTajCw676Sn:K29yOJp64eW0fB2usaGz8S4wPRbhD0G2wCffluAZfgHKjIkn4j1M-eT55-ExPt90mH2xBH-inHu2WDuGizV-UNV8a9ITKAPXU0zmGDmp4F-_jaHyGH3sOmgebtkaNOYA-5LUBK1pQIorjkMieGCZNet9V4i3e6PVBJtCuA==}
K3S_TOKEN = ${PROLE_SECRET:v1:8I31Aq_l-010FTgr:EF4jOYkqBhPKTN-jKRc_SQIIfCWVMUMw6niZplo2Yz8msZ9Zd6VpCuik-cpS0PydX7fl5H9AVoZtQxQQmA7LAQHaDrlFfGfl2LaArVvXTyzRmO7MRt2UoVhQ6s-t1Aw5DPiDM8tx10CXZ1-PGJvNHNTJ76XEpepOzxoQFw==}
MODE = k3s
PIPELINE_URL = http://myrddin.prole.org:8080
PIPELINE_URL = http://127.0.0.1:8080
[Prod Cluster (k8s)]
ARTIFACTS_DIR =
ARTIFACTS_DIR = /Users/chrisfu/dev/prole/data/staging
CLUSTER_ENV = prole-prod-cluster
DISPLAY_NAME = prole-prod-cluster
MODE = k8s
PIPELINE_URL = http://myrddin.prole.org:8080
PIPELINE_URL = http://127.0.0.1:8080
[Docker Build]
; No configuration values captured yet for this section.
[Initialization Scripts]
; No configuration values captured yet for this section.
STATUS = Attempted
[Deployment]
MODE = k3s
TARGET = prole-service-cluster
[Install]
STATUS = Finished
; No configuration values captured yet for this section.

View File

@ -24,7 +24,7 @@ fi
ACTION=${1:-start}
NAMESPACE="${PROLE_NAMESPACE}"
NAMESPACE="${PROLE_NAMESPACE:-${DATABASE_NAMESPACE:-${SERVICE_NAMESPACE:-knoe-db}}}"
CNPG_CLUSTER_NAME=${CNPG_CLUSTER_NAME:-knoe-db}
CNPG_OPERATOR_NAMESPACE=${CNPG_OPERATOR_NAMESPACE:-cnpg-system}
GARAGE_NAME=${GARAGE_NAME:-garage}

View File

@ -138,6 +138,12 @@ kubectl_apply_retry() {
done
}
# Sets KONG_CONFIG_CHANGED=1 in the caller's scope when the ConfigMap was
# created or updated; leaves it 0 when kubectl reported "unchanged".
# A temp file is used to communicate the result out of the subshell.
KONG_CONFIG_CHANGED=0
_KONG_CONFIG_CHANGED_FILE=""
create_kong_config() {
echo "Creating/updating Kong declarative config '$KONG_CONFIG_NAME' in namespace '$NAMESPACE' ..."
@ -194,6 +200,8 @@ KONGEOF
)
# Use a subshell to ensure the cleanup trap doesn't leak globally (and trip `set -u` later).
# A sentinel file is used to communicate a config change back to the parent shell.
_KONG_CONFIG_CHANGED_FILE="$(mktemp)"
(
tmp="$(mktemp)"
trap 'rm -f "${tmp:-}"' EXIT
@ -203,8 +211,18 @@ KONGEOF
--from-literal=kong.yml="$kong_yml" \
--dry-run=client -o yaml >"$tmp"
kubectl_apply_retry -f "$tmp"
local apply_out
apply_out=$(kubectl_apply_retry -f "$tmp" 2>&1)
echo "$apply_out"
if ! echo "$apply_out" | grep -q 'unchanged'; then
echo "1" >"$_KONG_CONFIG_CHANGED_FILE"
fi
)
if [[ -f "$_KONG_CONFIG_CHANGED_FILE" ]] && [[ "$(cat "$_KONG_CONFIG_CHANGED_FILE")" == "1" ]]; then
KONG_CONFIG_CHANGED=1
fi
rm -f "$_KONG_CONFIG_CHANGED_FILE"
_KONG_CONFIG_CHANGED_FILE=""
echo "ConfigMap '$KONG_CONFIG_NAME' ready."
}
@ -286,17 +304,36 @@ deploy() {
local manifests_dir="$PROLE_HOME/deploy/opentofu/k3s/manifests/prole"
kubectl_apply_retry -f "$manifests_dir/kong-deployment.yaml" -n "$NAMESPACE"
kubectl_apply_retry -f "$manifests_dir/kong-service.yaml" -n "$NAMESPACE"
# Determine whether the Deployment already exists before applying manifests.
local deployment_existed=0
kubectl_rt get deployment "$KONG_NAME" -n "$NAMESPACE" >/dev/null 2>&1 && deployment_existed=1
local deploy_out
deploy_out=$(kubectl_apply_retry -f "$manifests_dir/kong-deployment.yaml" -n "$NAMESPACE" 2>&1)
echo "$deploy_out"
local svc_out
svc_out=$(kubectl_apply_retry -f "$manifests_dir/kong-service.yaml" -n "$NAMESPACE" 2>&1)
echo "$svc_out"
echo "Waiting for $KONG_NAME rollout ..."
kubectl rollout status deployment/"$KONG_NAME" -n "$NAMESPACE" --timeout=120s
# ConfigMaps do not trigger a Deployment rollout by default; always restart
# Kong on update/start so it reloads the declarative config.
echo "Restarting $KONG_NAME to reload declarative config ..."
kubectl rollout restart deployment/"$KONG_NAME" -n "$NAMESPACE" >/dev/null 2>&1 || true
kubectl rollout status deployment/"$KONG_NAME" -n "$NAMESPACE" --timeout=120s >/dev/null 2>&1 || true
# ConfigMaps do not trigger a Deployment rollout by default. Only restart
# Kong when something actually changed: either this is a fresh deployment or
# the declarative config ConfigMap was modified. Skipping the restart when
# nothing changed prevents a new ReplicaSet from being created every run.
local need_restart=0
[[ "$deployment_existed" -eq 0 ]] && need_restart=1
[[ "${KONG_CONFIG_CHANGED:-0}" -eq 1 ]] && need_restart=1
if [[ "$need_restart" -eq 1 ]]; then
echo "Restarting $KONG_NAME to reload declarative config ..."
kubectl rollout restart deployment/"$KONG_NAME" -n "$NAMESPACE" >/dev/null 2>&1 || true
kubectl rollout status deployment/"$KONG_NAME" -n "$NAMESPACE" --timeout=120s >/dev/null 2>&1 || true
else
echo "$KONG_NAME config unchanged; skipping rollout restart."
fi
echo "$KONG_NAME deployed successfully."
}

View File

@ -357,8 +357,9 @@ class ProleInstaller(
("Database Creation", "init_password"),
("Initialization Scripts", "init_scripts"),
("Kerberos Authentication", "kerberos_config"),
("GitOps", "gitops_config"),
("GitOps Provider", "gitops_choice"),
("ArgoCD", "argocd_config"),
("GitOps / Gitea", "gitops_config"),
("Supabase", "supabase_config"),
("Deployment", "init_cnpg_deploy"),
("Post Install", "create_installer"),
@ -665,6 +666,16 @@ class ProleInstaller(
self.prod_artifacts_path = tk.StringVar(
value=str(PROJECT_ROOT / "data" / "staging")
)
# GitOps provider choice (gitea / argocd / none)
self.gitops_provider = tk.StringVar(value="gitea")
# Google Workspace
self.gworkspace_domain = tk.StringVar()
self.gworkspace_customer_id = tk.StringVar()
self.gworkspace_admin_email = tk.StringVar()
# Google Cloud
self.gcloud_project_id = tk.StringVar()
self.gcloud_region = tk.StringVar(value="us-central1")
self.gcloud_sa_key_path = tk.StringVar()
self.k3s_server_url = tk.StringVar()
self.k3s_token = tk.StringVar()
self.k3s_services_status = {
@ -767,6 +778,7 @@ class ProleInstaller(
self._register_canvas_renderer(
"supabase_config", self._render_supabase_config_page
)
self._register_canvas_renderer("gitops_choice", self._render_gitops_choice_page)
self._register_canvas_renderer("argocd_config", self._render_argocd_config_page)
self._register_canvas_renderer("gitops_config", self._render_gitops_config_page)
self._register_canvas_renderer("init_cluster", self._render_init_cluster_page)
@ -801,6 +813,7 @@ class ProleInstaller(
self._register_page("init_db_build", None)
self._register_page("init_scripts", None)
self._register_page("kerberos_config", None)
self._register_page("gitops_choice", None)
self._register_page("argocd_config", None)
self._register_page("gitops_config", None)
self._register_page("supabase_config", None)

View File

@ -329,6 +329,180 @@ class ClusterScreenMixin:
fill="#6e6e73",
font=("SF Pro Text", 10),
)
y += 40
# ── GitOps Provider ────────────────────────────────────
self._canvas_items.append(
ui.canvas_text(
self,
x_label,
y,
"GitOps Provider:",
fill="black",
font=("SF Pro Text", 12, "bold"),
)
)
y += 4
for label, value in [
("Gitea", "gitea"),
("ArgoCD", "argocd"),
("None", "none"),
]:
rb = tk.Radiobutton(
self.bg_canvas,
text=label,
variable=self.gitops_provider,
value=value,
bg="white",
fg="black",
activebackground="white",
selectcolor="white",
font=("SF Pro Text", 11),
)
rb_win = self.bg_canvas.create_window(
x_label + 160 + ["gitea", "argocd", "none"].index(value) * 110,
y - 2,
window=rb,
anchor="nw",
)
self._canvas_items.append(rb_win)
self._overlay_widgets.append(rb)
y += 34
# ── Google Workspace ───────────────────────────────────
self._canvas_items.append(
ui.canvas_text(
self,
x_label,
y,
"Google Workspace",
fill="black",
font=("SF Pro Text", 12, "bold"),
)
)
y += 28
for field_label, var, placeholder in [
("Domain:", self.gworkspace_domain, "example.com"),
("Customer ID:", self.gworkspace_customer_id, "C0xxxxxxx"),
("Admin Email:", self.gworkspace_admin_email, "admin@example.com"),
]:
self._canvas_items.append(
ui.canvas_text(
self,
x_label + 20,
y,
field_label,
fill="black",
font=("SF Pro Text", 11),
)
)
gw_entry = tk.Entry(
self.bg_canvas,
textvariable=var,
width=36,
bg="white",
fg="black",
insertbackground="black",
highlightbackground="#CCCCCC",
highlightthickness=1,
relief="flat",
font=("SF Pro Text", 11),
)
gw_win = self.bg_canvas.create_window(
x_label + 160, y - 6, window=gw_entry, anchor="nw"
)
self._canvas_items.append(gw_win)
self._overlay_widgets.append(gw_entry)
y += 30
y += 6
# ── Google Cloud ───────────────────────────────────────
self._canvas_items.append(
ui.canvas_text(
self,
x_label,
y,
"Google Cloud",
fill="black",
font=("SF Pro Text", 12, "bold"),
)
)
y += 28
for field_label, var in [
("Project ID:", self.gcloud_project_id),
("Region:", self.gcloud_region),
]:
self._canvas_items.append(
ui.canvas_text(
self,
x_label + 20,
y,
field_label,
fill="black",
font=("SF Pro Text", 11),
)
)
gc_entry = tk.Entry(
self.bg_canvas,
textvariable=var,
width=36,
bg="white",
fg="black",
insertbackground="black",
highlightbackground="#CCCCCC",
highlightthickness=1,
relief="flat",
font=("SF Pro Text", 11),
)
gc_win = self.bg_canvas.create_window(
x_label + 160, y - 6, window=gc_entry, anchor="nw"
)
self._canvas_items.append(gc_win)
self._overlay_widgets.append(gc_entry)
y += 30
# Service Account Key
self._canvas_items.append(
ui.canvas_text(
self,
x_label + 20,
y,
"SA Key Path:",
fill="black",
font=("SF Pro Text", 11),
)
)
sa_entry = tk.Entry(
self.bg_canvas,
textvariable=self.gcloud_sa_key_path,
width=36,
bg="white",
fg="black",
insertbackground="black",
highlightbackground="#CCCCCC",
highlightthickness=1,
relief="flat",
font=("SF Pro Text", 11),
)
sa_win = self.bg_canvas.create_window(
x_label + 160, y - 6, window=sa_entry, anchor="nw"
)
self._canvas_items.append(sa_win)
self._overlay_widgets.append(sa_entry)
sa_browse = tk.Button(
self.bg_canvas,
text="Browse...",
command=lambda: self.gcloud_sa_key_path.set(
filedialog.askopenfilename(
filetypes=[("JSON", "*.json"), ("All", "*")]
)
or self.gcloud_sa_key_path.get()
),
)
sa_browse_win = self.bg_canvas.create_window(
x_label + 580, y - 8, window=sa_browse, anchor="nw"
)
self._canvas_items.append(sa_browse_win)
self._overlay_widgets.append(sa_browse)
y += 34
# Common Services Status Check (Notebook)

View File

@ -15,6 +15,88 @@ from knoe.core.env import PROJECT_ROOT, _normalize_cluster_env, _parse_bool
class GitOpsScreenMixin:
"""GitOps / Gitea deployment screen."""
# ------------------------------------------------------------------
# GitOps provider choice screen
# ------------------------------------------------------------------
def _render_gitops_choice_page(self):
"""Explicit GitOps provider selection: Gitea, ArgoCD, or None."""
content_width = self.bg_canvas.winfo_width() or 975
right_margin = content_width - 48
ui.canvas_text(
self,
right_margin,
40,
"Prole",
fill="#6e6e73",
font=("SF Pro Text", 32, "bold"),
anchor="ne",
)
ui.canvas_text(
self,
right_margin,
85,
"Infrastructure Automated.",
fill="#6e6e73",
font=("SF Pro Text", 18),
anchor="ne",
)
self._render_title("GitOps Provider", y=150)
description = (
"Choose your GitOps provider for this deployment.\n"
"Gitea deploys a self-hosted Git server; ArgoCD adds continuous delivery.\n"
"Select None to skip GitOps tooling entirely."
)
self._render_paragraph(description, y=210)
x_label = 48
y = 310
for label, value in [
("Gitea — self-hosted Git + CI (recommended)", "gitea"),
("ArgoCD — continuous delivery / GitOps sync", "argocd"),
("None — skip GitOps tooling", "none"),
]:
rb = tk.Radiobutton(
self.bg_canvas,
text=label,
variable=self.gitops_provider,
value=value,
command=self._on_gitops_provider_change,
bg="white",
fg="black",
activebackground="white",
selectcolor="white",
font=("SF Pro Text", 12),
)
win = self.bg_canvas.create_window(x_label, y, window=rb, anchor="nw")
self._canvas_items.append(win)
self._overlay_widgets.append(rb)
y += 36
def _on_gitops_provider_change(self):
"""Persist the chosen GitOps provider to prole_cfg."""
provider = self.gitops_provider.get()
try:
if "Optional Features" not in self.prole_cfg_data:
self.prole_cfg_data["Optional Features"] = {}
self.prole_cfg_data["Optional Features"]["GITOPS_PROVIDER"] = provider
# Keep legacy flags in sync so downstream steps remain consistent.
self.prole_cfg_data["Optional Features"]["GITOPS_ENABLED"] = str(
provider == "gitea"
)
self.prole_cfg_data["Optional Features"]["ARGOCD_ENABLED"] = str(
provider == "argocd"
)
self._save_prole_cfg()
except Exception:
pass
# Sync boolean vars used by the individual feature screens.
self.gitops_enabled.set(provider == "gitea")
self.argocd_enabled.set(provider == "argocd")
# ------------------------------------------------------------------
# Gitea deployment screen
# ------------------------------------------------------------------
def _render_gitops_config_page(self):
content_width = self.bg_canvas.winfo_width() or 975
right_margin = content_width - 48

View File

@ -345,9 +345,12 @@ class NavigationMixin:
if current_id == "kerberos_config":
self.show_page("init_scripts")
return
if current_id == "argocd_config":
if current_id == "gitops_choice":
self.show_page("kerberos_config")
return
if current_id == "argocd_config":
self.show_page("gitops_choice")
return
if current_id == "gitops_config":
self.show_page("argocd_config")
return
@ -592,7 +595,7 @@ class NavigationMixin:
if self.kerberos_enabled.get():
self.show_page("kerberos_config")
else:
self.show_page("argocd_config")
self.show_page("gitops_choice")
return
if current_id == "kerberos_config":
@ -634,9 +637,17 @@ class NavigationMixin:
os.environ.get("KRB5_AD_SERVICE_NAME", "prole-kerberos-ad-dc")
)
self._save_prole_cfg()
self.show_page("gitops_choice")
return
if current_id == "gitops_choice":
if "Optional Features" not in self.prole_cfg_data:
self.prole_cfg_data["Optional Features"] = {}
self.prole_cfg_data["Optional Features"]["GITOPS_PROVIDER"] = (
self.gitops_provider.get()
)
self._save_prole_cfg()
self.show_page("argocd_config")
return
if current_id == "argocd_config":
self.prole_cfg_data["Optional Features"]["ARGOCD_ENABLED"] = str(
self.argocd_enabled.get()
@ -787,6 +798,8 @@ class NavigationMixin:
self.next_button.configure(state="disabled")
else:
self.next_button.configure(state="normal")
elif pid == "gitops_choice":
self.next_button.configure(state="normal")
elif pid == "argocd_config":
if self.argocd_enabled.get():
if getattr(self, "_argocd_success", False):

View File

@ -166,11 +166,11 @@ class ServicesScreenMixin:
("Kubectl Status", self.KUBECTL_STATUS_TAB),
("Common Services", "init_common_services.sh"),
("Common Services Log", "common_services_log"),
("Cert-Manager", "init_certmgr.sh"),
("CloudNative-PG", "init_cloudnative_pg.sh"),
]
scripts.append(("Prole DB Backup", "init_cnpg_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":
@ -224,6 +224,7 @@ class ServicesScreenMixin:
env["DB_PASSWORD"] = password
env["GRAFANA_ADMIN_PASSWORD"] = password
env["NAMESPACE"] = (self.db_namespace.get() or "").strip()
env["PROLE_NAMESPACE"] = env["NAMESPACE"]
env["SERVICE_NAMESPACE"] = self._get_service_namespace()
if self.kerberos_realm.get().strip():
env["KRB5_REALM"] = self.kerberos_realm.get().strip()
@ -284,13 +285,13 @@ class ServicesScreenMixin:
except Exception:
pass
# 1. init_common_services.sh update
# 1. init_common_services.sh start
overall_success = True
if overall_success:
script = "init_common_services.sh"
_select_tab(script)
self.script_consoles[script].clear()
self.script_consoles[script].write(f"Running {script} update...\n")
self.script_consoles[script].write(f"Running {script} start...\n")
svc_log_path = _log_path_for(script)
self._last_common_services_log_path = svc_log_path
@ -319,9 +320,10 @@ class ServicesScreenMixin:
pass
svc_args = list(mode_args)
svc_args += ["-n", env.get("NAMESPACE") or env.get("SERVICE_NAMESPACE") or "default"]
if self.kerberos_enabled.get():
svc_args.append("-k")
svc_args.append("update")
svc_args.append("start")
rc_svc = self.controller.run_script(
script, args=svc_args, env=env, on_line=_svc_line
)
@ -333,7 +335,7 @@ class ServicesScreenMixin:
if rc_svc != 0:
self.script_consoles[script].write(
f"\nERROR: {script} update failed with code {rc_svc}\n"
f"\nERROR: {script} start failed with code {rc_svc}\n"
)
overall_success = False
else:
@ -345,60 +347,7 @@ class ServicesScreenMixin:
"Skipping common services initialization because previous steps failed.\n"
)
# 2. init_certmgr.sh initialize
if overall_success:
script = "init_certmgr.sh"
_select_tab(script)
self.script_consoles[script].clear()
self.script_consoles[script].write(f"Running {script} initialize...\n")
self.script_consoles[script].write(
"> bash etc/init_certmgr.sh initialize\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 _certmgr_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_cm = self.controller.run_script(
script,
args=mode_args + ["initialize"],
env=env,
on_line=_certmgr_line,
)
if log_fp:
try:
log_fp.close()
except Exception:
pass
if rc_cm != 0:
self.script_consoles[script].write(
f"\nERROR: {script} initialize failed with code {rc_cm}\n"
)
overall_success = False
else:
self.script_consoles[script].write(
f"\n{script} completed successfully.\n"
)
else:
self.script_consoles["init_certmgr.sh"].write(
"Skipping cert-manager initialization because previous steps failed.\n"
)
# 3. CloudNative-PG initialization (Python)
# 2. CloudNative-PG initialization (Python)
cnpg_tab = "init_cloudnative_pg.sh"
if overall_success:
_select_tab(cnpg_tab)
@ -527,8 +476,9 @@ class ServicesScreenMixin:
except Exception:
pass
kong_ns = env.get("NAMESPACE") or "knoe-db"
rc_kong = self.controller.run_script(
script, args=mode_args + ["start"], env=env, on_line=_kong_line
script, args=mode_args + ["-n", kong_ns, "start"], env=env, on_line=_kong_line
)
if log_fp:
try:
@ -551,6 +501,55 @@ class ServicesScreenMixin:
"Skipping Kong because previous steps failed.\n"
)
# 6. 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"
)
# 7. init_monitoring.sh initialize
if overall_success and opt_allowed:
script = "init_monitoring.sh"

View File

@ -130,6 +130,12 @@ kubectl_apply_retry() {
done
}
# Sets KONG_CONFIG_CHANGED=1 in the caller's scope when the ConfigMap was
# created or updated; leaves it 0 when kubectl reported "unchanged".
# A temp file is used to communicate the result out of the subshell.
KONG_CONFIG_CHANGED=0
_KONG_CONFIG_CHANGED_FILE=""
create_kong_config() {
echo "Creating/updating Kong declarative config '$KONG_CONFIG_NAME' in namespace '$NAMESPACE' ..."
@ -168,6 +174,8 @@ KONGEOF
)
# Use a subshell to ensure the cleanup trap doesn't leak globally (and trip `set -u` later).
# A sentinel file is used to communicate a config change back to the parent shell.
_KONG_CONFIG_CHANGED_FILE="$(mktemp)"
(
tmp="$(mktemp)"
trap 'rm -f "${tmp:-}"' EXIT
@ -177,8 +185,18 @@ KONGEOF
--from-literal=kong.yml="$kong_yml" \
--dry-run=client -o yaml >"$tmp"
kubectl_apply_retry -f "$tmp"
local apply_out
apply_out=$(kubectl_apply_retry -f "$tmp" 2>&1)
echo "$apply_out"
if ! echo "$apply_out" | grep -q 'unchanged'; then
echo "1" >"$_KONG_CONFIG_CHANGED_FILE"
fi
)
if [[ -f "$_KONG_CONFIG_CHANGED_FILE" ]] && [[ "$(cat "$_KONG_CONFIG_CHANGED_FILE")" == "1" ]]; then
KONG_CONFIG_CHANGED=1
fi
rm -f "$_KONG_CONFIG_CHANGED_FILE"
_KONG_CONFIG_CHANGED_FILE=""
echo "ConfigMap '$KONG_CONFIG_NAME' ready."
}
@ -238,17 +256,36 @@ deploy() {
local manifests_dir="$PROLE_HOME/deploy/opentofu/k3s/manifests/prole"
kubectl_apply_retry -f "$manifests_dir/kong-deployment.yaml" -n "$NAMESPACE"
kubectl_apply_retry -f "$manifests_dir/kong-service.yaml" -n "$NAMESPACE"
# Determine whether the Deployment already exists before applying manifests.
local deployment_existed=0
kubectl_rt get deployment "$KONG_NAME" -n "$NAMESPACE" >/dev/null 2>&1 && deployment_existed=1
local deploy_out
deploy_out=$(kubectl_apply_retry -f "$manifests_dir/kong-deployment.yaml" -n "$NAMESPACE" 2>&1)
echo "$deploy_out"
local svc_out
svc_out=$(kubectl_apply_retry -f "$manifests_dir/kong-service.yaml" -n "$NAMESPACE" 2>&1)
echo "$svc_out"
echo "Waiting for $KONG_NAME rollout ..."
kubectl rollout status deployment/"$KONG_NAME" -n "$NAMESPACE" --timeout=120s
# ConfigMaps do not trigger a Deployment rollout by default; always restart
# Kong on update/start so it reloads the declarative config.
echo "Restarting $KONG_NAME to reload declarative config ..."
kubectl rollout restart deployment/"$KONG_NAME" -n "$NAMESPACE" >/dev/null 2>&1 || true
kubectl rollout status deployment/"$KONG_NAME" -n "$NAMESPACE" --timeout=120s >/dev/null 2>&1 || true
# ConfigMaps do not trigger a Deployment rollout by default. Only restart
# Kong when something actually changed: either this is a fresh deployment or
# the declarative config ConfigMap was modified. Skipping the restart when
# nothing changed prevents a new ReplicaSet from being created every run.
local need_restart=0
[[ "$deployment_existed" -eq 0 ]] && need_restart=1
[[ "${KONG_CONFIG_CHANGED:-0}" -eq 1 ]] && need_restart=1
if [[ "$need_restart" -eq 1 ]]; then
echo "Restarting $KONG_NAME to reload declarative config ..."
kubectl rollout restart deployment/"$KONG_NAME" -n "$NAMESPACE" >/dev/null 2>&1 || true
kubectl rollout status deployment/"$KONG_NAME" -n "$NAMESPACE" --timeout=120s >/dev/null 2>&1 || true
else
echo "$KONG_NAME config unchanged; skipping rollout restart."
fi
echo "$KONG_NAME deployed successfully."
}

View File

@ -1 +1 @@
3
4

View File

@ -1,47 +1,51 @@
Network Discovery Summary:
Primary Router: 10.0.0.1 (eero_5d:50:f2)
DNS Servers: 10.0.0.5, 10.0.0.4, 100.100.100.100
DNS Servers: 10.0.0.5, 100.100.100.100, 10.0.0.4
Detected Devices:
- 10.0.0.38 [7e:19:9a:fe:9f:ed] (7e:19:9a:fe:9f:ed): Ports [22, 445, 5900, 11434, 88], Services: ['SSH', 'VNC', 'SMB/CIFS (Possible Windows/AD)', 'Ollama', 'Active Directory Related']
- 10.0.0.1 [9c:57:bc:5d:50:f2] (eero_5d:50:f2): Ports [], Services: []
- 10.0.0.33 [ec:b5:fa:b0:76:e4] (PhilipsLight_b0:76:e4): Ports [80, 443], Services: ['Web Server']
- 10.0.0.206 [00:a0:de:a2:0b:ef] (Yamaha_a2:0b:ef): Ports [80], Services: ['Web Server']
- 10.0.0.189 [00:17:88:a3:2f:cc] (PhilipsLight_a3:2f:cc): Ports [80, 443], Services: ['Web Server']
- 10.0.0.179 [48:a6:b8:a4:8e:cc] (Sonos_a4:8e:cc): Ports [], Services: []
- 10.0.0.38\ [7e:19:9a:fe:9f:ed] (7e:19:9a:fe:9f:ed): Ports [], Services: []
- 10.0.0.188 [00:16:6c:c5:1f:54] (SamsungElect_c5:1f:54): Ports [80, 443], Services: ['Web Server']
- 10.0.0.4 [b8:27:eb:b3:6f:2b] (RaspberryPiF_b3:6f:2b): Ports [22, 53, 2049], Services: ['DNS', 'SSH', 'NFS']
- 10.0.0.3 [2c:cf:67:8c:2b:47] (RaspberryPi_8c:2b:47): Ports [22, 53, 80, 443, 445, 5900, 88, 389, 636], Services: ['DNS', 'SSH', 'VNC', 'Web Server', 'SMB/CIFS (Possible Windows/AD)', 'Active Directory Related']
- 10.0.0.5 [b8:27:eb:88:3a:41] (RaspberryPiF_88:3a:41): Ports [22, 53, 2049, 5900], Services: ['DNS', 'SSH', 'NFS', 'VNC']
- 10.0.0.205 [a8:20:66:28:12:e7] (Apple_28:12:e7): Ports [22, 445, 5900, 88], Services: ['SSH', 'VNC', 'SMB/CIFS (Possible Windows/AD)', 'Active Directory Related']
- 10.0.0.170 [b0:ee:7b:ca:73:99] (Roku_ca:73:99): Ports [], Services: []
- 10.0.0.203 [00:11:32:3b:2f:08] (Synology_3b:2f:08): Ports [22, 80, 443, 2049, 445], Services: ['SSH', 'NFS', 'Web Server', 'SMB/CIFS (Possible Windows/AD)']
- 10.0.0.1 [9c:57:bc:5d:50:f2] (eero_5d:50:f2): Ports [53], Services: ['DNS']
- 10.0.0.3 [2c:cf:67:8c:2b:47] (RaspberryPi_8c:2b:47): Ports [22, 53, 80, 443, 2049, 445, 5900, 88, 389, 636], Services: ['DNS', 'SSH', 'NFS', 'VNC', 'Web Server', 'SMB/CIFS (Possible Windows/AD)', 'Active Directory Related']
- 10.0.0.4 [b8:27:eb:b3:6f:2b] (RaspberryPiF_b3:6f:2b): Ports [22, 53, 443, 2049], Services: ['DNS', 'SSH', 'NFS', 'Web Server']
- 10.0.0.5 [b8:27:eb:88:3a:41] (RaspberryPiF_88:3a:41): Ports [22, 53, 443, 2049, 5900], Services: ['DNS', 'SSH', 'NFS', 'VNC', 'Web Server']
- 10.0.0.6 [2c:cf:67:45:8d:97] (RaspberryPi_45:8d:97): Ports [22, 80, 443], Services: ['SSH', 'Web Server']
- 10.0.0.2 [dc:a4:ca:ea:1a:2f] (Apple_ea:1a:2f): Ports [], Services: []
- 10.0.0.6 [2c:cf:67:45:8d:97] (RaspberryPi_45:8d:97): Ports [22, 443], Services: ['SSH', 'Web Server']
- 10.0.0.22 [2c:cf:67:45:8d:97] (RaspberryPi_45:8d:97): Ports [22, 443], Services: ['SSH', 'Web Server']
- 10.0.0.22 [2c:cf:67:45:8d:97] (RaspberryPi_45:8d:97): Ports [22, 80, 443], Services: ['SSH', 'Web Server']
- 10.0.0.26 [c8:db:26:08:52:f8] (Logitech_08:52:f8): Ports [], Services: []
- 10.0.0.205 [a8:20:66:28:12:e7] (Apple_28:12:e7): Ports [22, 445, 5900, 88], Services: ['SSH', 'VNC', 'SMB/CIFS (Possible Windows/AD)', 'Active Directory Related']
- 10.0.0.33 [ec:b5:fa:b0:76:e4] (PhilipsLight_b0:76:e4): Ports [80, 443], Services: ['Web Server']
- 10.0.0.48 [0c:fe:45:53:f4:3c] (SonyInteract_53:f4:3c): Ports [], Services: []
- 10.0.0.37 [54:07:7d:22:c0:b2] (Netgear_22:c0:b2): Ports [80], Services: ['Web Server']
- 10.0.0.55 [48:a6:b8:a7:50:60] (Sonos_a7:50:60): Ports [], Services: []
- 10.0.0.45 [f8:b4:6a:30:a4:1f] (HewlettPacka_30:a4:1f): Ports [80, 443], Services: ['Web Server']
- 10.0.0.46 [d4:f7:d5:40:ab:17] (SonyInteract_40:ab:17): Ports [], Services: []
- 10.0.0.41 [b8:27:eb:57:10:d9] (RaspberryPiF_57:10:d9): Ports [22], Services: ['SSH']
- 10.0.0.206 [00:a0:de:a2:0b:ef] (Yamaha_a2:0b:ef): Ports [80], Services: ['Web Server']
- 10.0.0.73 [4c:a9:19:b3:12:f8] (TuyaSmart_b3:12:f8): Ports [], Services: []
- 10.0.0.1\ [9c:57:bc:5d:50:f2] (eero_5d:50:f2): Ports [], Services: []
- 10.0.0.95 [b8:27:eb:88:3a:41] (RaspberryPiF_88:3a:41): Ports [22, 53, 443, 2049, 5900], Services: ['DNS', 'SSH', 'NFS', 'VNC', 'Web Server']
- 10.0.0.107 [b8:06:0d:b7:7c:56] (TuyaSmart_b7:7c:56): Ports [], Services: []
- 10.0.0.113 [60:81:10:92:02:4e] (Apple_92:02:4e): Ports [], Services: []
- 10.0.0.94 [f0:20:ff:bf:9a:19] (Intel_bf:9a:19): Ports [], Services: []
Note: Potential host for NVIDIA cards (PC Hardware vendor)
- 10.0.0.175 [28:80:88:e4:10:1a] (Netgear_e4:10:1a): Ports [80], Services: ['Web Server']
- 10.0.0.123 [3c:ef:8c:96:e3:3c] (ZhejiangDahu_96:e3:3c): Ports [80], Services: ['Web Server']
- 10.0.0.124 [9c:8e:cd:02:73:c4] (AmcrestTechn_02:73:c4): Ports [80], Services: ['Web Server']
- 10.0.0.130 [f8:bb:bf:a3:84:eb] (eero_a3:84:eb): Ports [53], Services: ['DNS']
- 10.0.0.111 [ca:0b:6c:5d:0a:e5] (ca:0b:6c:5d:0a:e5): Ports [], Services: []
- 10.0.0.127 [58:55:ca:4a:60:6e] (Apple_4a:60:6e): Ports [22], Services: ['SSH']
- 10.0.0.125 [a0:60:32:04:73:ba] (AmcrestTechn_04:73:ba): Ports [80], Services: ['Web Server']
- 10.0.0.117 [40:f5:20:1e:5e:91] (Espressif_1e:5e:91): Ports [80], Services: ['Web Server']
- 10.0.0.130 [f8:bb:bf:a3:84:eb] (eero_a3:84:eb): Ports [53], Services: ['DNS']
- 10.0.0.107 [b8:06:0d:b7:7c:56] (TuyaSmart_b7:7c:56): Ports [], Services: []
- 10.0.0.143 [60:5f:8d:7a:aa:32] (eero_7a:aa:32): Ports [53], Services: ['DNS']
- 10.0.0.145 [7c:a6:b0:04:db:1c] (7c:a6:b0:04:db:1c): Ports [], Services: []
- 10.0.0.155 [62:17:97:b3:06:a1] (62:17:97:b3:06:a1): Ports [], Services: []
- 10.0.0.175 [28:80:88:e4:10:1a] (Netgear_e4:10:1a): Ports [80], Services: ['Web Server']
- 10.0.0.208 [a0:ad:9f:30:84:5f] (ASUSTekCOMPU_30:84:5f): Ports [], Services: []
- 10.0.0.170 [b0:ee:7b:ca:73:99] (Roku_ca:73:99): Ports [], Services: []
- 10.0.0.179 [48:a6:b8:a4:8e:cc] (Sonos_a4:8e:cc): Ports [], Services: []
- 10.0.0.189 [00:17:88:a3:2f:cc] (PhilipsLight_a3:2f:cc): Ports [80, 443], Services: ['Web Server']
- 10.0.0.188 [00:16:6c:c5:1f:54] (SamsungElect_c5:1f:54): Ports [80, 443], Services: ['Web Server']
- 10.0.0.196 [60:5f:8d:88:08:f2] (eero_88:08:f2): Ports [53], Services: ['DNS']
- 10.0.0.203 [00:11:32:3b:2f:08] (Synology_3b:2f:08): Ports [22, 80, 443, 2049, 445], Services: ['SSH', 'NFS', 'Web Server', 'SMB/CIFS (Possible Windows/AD)']
- 10.0.0.199 [7c:a6:b0:01:a7:33] (7c:a6:b0:01:a7:33): Ports [], Services: []
- 10.0.0.204 [50:eb:f6:56:f3:26] (ASUSTekCOMPU_56:f3:26): Ports [22, 3389, 445, 5900, 11434], Services: ['SSH', 'VNC', 'SMB/CIFS (Possible Windows/AD)', 'RDP (Windows)', 'Ollama']
- 10.0.0.180 [4c:e1:73:42:1a:be] (HuizhouDehon_2:1a:be): Ports [445], Services: ['SMB/CIFS (Possible Windows/AD)']
- 10.0.0.208 [a0:ad:9f:30:84:5f] (ASUSTekCOMPU_30:84:5f): Ports [], Services: []
- 10.0.0.113 [60:81:10:92:02:4e] (Apple_92:02:4e): Ports [], Services: []
Ollama Instances found at: 10.0.0.38, 10.0.0.204
Ansible Inventory Summary:

View File

@ -210,7 +210,7 @@ deployment:
resources: {}
db:
enabled: true
enabled: false
replicaCount: 1
nameOverride: ""
fullnameOverride: ""
@ -225,7 +225,6 @@ deployment:
volumeMounts: {}
volumes: {}
resources: {}
functions:
enabled: true
replicaCount: 1
@ -261,7 +260,7 @@ deployment:
resources: {}
kong:
enabled: true
enabled: false
replicaCount: 1
nameOverride: ""
fullnameOverride: ""
@ -276,7 +275,6 @@ deployment:
volumeMounts: {}
volumes: {}
resources: {}
meta:
enabled: true
replicaCount: 1
@ -346,7 +344,7 @@ deployment:
resources: {}
storage:
enabled: true
enabled: false
replicaCount: 1
nameOverride: ""
fullnameOverride: ""
@ -361,9 +359,8 @@ deployment:
volumeMounts: {}
volumes: {}
resources: {}
studio:
enabled: true
enabled: false
replicaCount: 1
nameOverride: ""
fullnameOverride: ""
@ -378,7 +375,6 @@ deployment:
volumeMounts: {}
volumes: {}
resources: {}
vector:
enabled: true
replicaCount: 1
@ -396,6 +392,12 @@ deployment:
volumes: {}
resources: {}
## External Kong — shared Kong gateway in kube-system (local kong deployment is disabled)
externalKong:
enabled: true
host: kong-proxy.kube-system.svc.cluster.local
port: 80
externalDatabase:
enabled: false
host: ""
@ -627,10 +629,14 @@ environment:
DB_SSL: disable
REQUEST_ALLOW_X_FORWARDED_PATH: "true"
FILE_SIZE_LIMIT: "52428800"
FILE_STORAGE_BACKEND_PATH: /var/lib/storage
## Storage backend: rewired to shared Garage S3 (knoe-system)
STORAGE_BACKEND: s3
TENANT_ID: stub
REGION: stub
GLOBAL_S3_BUCKET: stub
REGION: garage
GLOBAL_S3_BUCKET: supabase-storage
GLOBAL_S3_ENDPOINT: http://garage.knoe-system.svc.cluster.local:3900
GLOBAL_S3_PROTOCOL: http
GLOBAL_S3_FORCE_PATH_STYLE: "true"
ENABLE_IMAGE_TRANSFORMATION: "true"
studio:

View File

@ -29,6 +29,14 @@ class _DummyClusterApp(ScreenBaseMixin, ClusterScreenMixin):
self.selected_kubectx = tk.StringVar(master=root, value="")
self.service_namespace = tk.StringVar(master=root, value="knoe-system")
self.prod_artifacts_path = tk.StringVar(master=root, value="/tmp")
# GitOps / GWorkspace / GCloud variables added to prod screen
self.gitops_provider = tk.StringVar(master=root, value="gitea")
self.gworkspace_domain = tk.StringVar(master=root, value="")
self.gworkspace_customer_id = tk.StringVar(master=root, value="")
self.gworkspace_admin_email = tk.StringVar(master=root, value="")
self.gcloud_project_id = tk.StringVar(master=root, value="")
self.gcloud_region = tk.StringVar(master=root, value="us-central1")
self.gcloud_sa_key_path = tk.StringVar(master=root, value="")
# Legacy variables still referenced by non-UI logic elsewhere
self.k3s_server_url = tk.StringVar(master=root, value="")
@ -176,9 +184,15 @@ def test_kubectx_selection_requires_apply():
self.selected_kubectx = tk.StringVar(master=root, value="ctx-000")
self.service_namespace = tk.StringVar(master=root, value="knoe-system")
self.prod_artifacts_path = tk.StringVar(master=root, value="/tmp")
self.gitops_provider = tk.StringVar(master=root, value="gitea")
self.gworkspace_domain = tk.StringVar(master=root, value="")
self.gworkspace_customer_id = tk.StringVar(master=root, value="")
self.gworkspace_admin_email = tk.StringVar(master=root, value="")
self.gcloud_project_id = tk.StringVar(master=root, value="")
self.gcloud_region = tk.StringVar(master=root, value="us-central1")
self.gcloud_sa_key_path = tk.StringVar(master=root, value="")
self.k3s_server_url = tk.StringVar(master=root, value="")
self.k3s_token = tk.StringVar(master=root, value="")
self._kubectx_applied = "ctx-000"
self._switch_calls: list[str] = []
@ -231,3 +245,29 @@ def test_kubectx_selection_requires_apply():
assert app._switch_calls == ["ctx-001"]
finally:
root.destroy()
def test_prod_cluster_screen_renders_google_and_gitops_fields():
"""Prod env must show GitOps Provider, Google Workspace, and Google Cloud fields."""
root = tk.Tk()
root.withdraw()
try:
canvas = tk.Canvas(root, width=975, height=900, bg="white")
canvas.pack(fill="both", expand=False)
root.update()
app = _DummyClusterApp(root, canvas, kubectx_values=["ctx-prod"])
app.cluster_env.set("prod")
app._render_init_cluster_page()
root.update()
texts = _canvas_texts(canvas)
assert "GitOps Provider:" in texts
assert "Google Workspace" in texts
assert "Google Cloud" in texts
assert "Domain:" in texts
assert "Customer ID:" in texts
assert "Admin Email:" in texts
assert "Project ID:" in texts
assert "Region:" in texts
assert "SA Key Path:" in texts
finally:
root.destroy()

View File

@ -0,0 +1,91 @@
"""Tests for the GitOps provider choice screen (_render_gitops_choice_page)."""
from __future__ import annotations
import tkinter as tk
from pathlib import Path
import sys
import pytest
PROJECT_ROOT = Path(__file__).resolve().parents[2]
if str(PROJECT_ROOT) not in sys.path:
sys.path.insert(0, str(PROJECT_ROOT))
from knoe.ui.screens.base import ScreenBaseMixin
from knoe.ui.screens.gitops import GitOpsScreenMixin
class _DummyGitOpsApp(ScreenBaseMixin, GitOpsScreenMixin):
"""Minimal surface to render the GitOps choice screen."""
def __init__(self, root: tk.Tk, canvas: tk.Canvas):
self.root = root
self.bg_canvas = canvas
self._canvas_items: list[int] = []
self._overlay_widgets: list[tk.Widget] = []
self.gitops_provider = tk.StringVar(master=root, value="gitea")
self.gitops_enabled = tk.BooleanVar(master=root, value=False)
self.argocd_enabled = tk.BooleanVar(master=root, value=False)
self.prole_cfg_data: dict = {}
def _save_prole_cfg(self):
pass
def _canvas_texts(cnv: tk.Canvas) -> list[str]:
return [
cnv.itemcget(item, "text")
for item in cnv.find_all()
if cnv.type(item) == "text"
]
def test_gitops_choice_page_renders_title_and_options():
"""The choice screen must render a title and all three provider options."""
root = tk.Tk()
root.withdraw()
try:
canvas = tk.Canvas(root, width=975, height=600, bg="white")
canvas.pack(fill="both", expand=False)
root.update()
app = _DummyGitOpsApp(root, canvas)
app._render_gitops_choice_page()
root.update()
texts = _canvas_texts(canvas)
assert "GitOps Provider" in texts
# All three provider labels must appear as Radiobutton text (widget titles).
widget_texts = [w.cget("text") for w in app._overlay_widgets if isinstance(w, tk.Radiobutton)]
assert any("Gitea" in t for t in widget_texts)
assert any("ArgoCD" in t for t in widget_texts)
assert any("None" in t for t in widget_texts)
finally:
root.destroy()
def test_on_gitops_provider_change_syncs_boolean_vars():
"""Selecting a provider must update gitops_enabled and argocd_enabled accordingly."""
root = tk.Tk()
root.withdraw()
try:
canvas = tk.Canvas(root, width=975, height=600, bg="white")
canvas.pack()
root.update()
app = _DummyGitOpsApp(root, canvas)
app.gitops_provider.set("gitea")
app._on_gitops_provider_change()
assert app.gitops_enabled.get() is True
assert app.argocd_enabled.get() is False
assert app.prole_cfg_data["Optional Features"]["GITOPS_PROVIDER"] == "gitea"
app.gitops_provider.set("argocd")
app._on_gitops_provider_change()
assert app.gitops_enabled.get() is False
assert app.argocd_enabled.get() is True
assert app.prole_cfg_data["Optional Features"]["ARGOCD_ENABLED"] == "True"
app.gitops_provider.set("none")
app._on_gitops_provider_change()
assert app.gitops_enabled.get() is False
assert app.argocd_enabled.get() is False
assert app.prole_cfg_data["Optional Features"]["GITOPS_PROVIDER"] == "none"
finally:
root.destroy()