diff --git a/conf/port-mapping.cfg b/conf/port-mapping.cfg index 66b815b..8ad477c 100644 --- a/conf/port-mapping.cfg +++ b/conf/port-mapping.cfg @@ -2,11 +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 -gitea-http: local=13000 remote=3000 ns=gitea svc=gitea-http address=0.0.0.0 gitea-ssh: local=22 remote=22 ns=gitea svc=gitea-ssh 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 @@ -15,4 +10,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 +gitea-http: local=13000 remote=3000 ns=gitea svc=gitea-http address=0.0.0.0 diff --git a/conf/prod/gcp.cfg b/conf/prod/gcp.cfg new file mode 100644 index 0000000..b00812e --- /dev/null +++ b/conf/prod/gcp.cfg @@ -0,0 +1,16 @@ +# Generated by knoe/config.py --mode k8s --provider gcp +# Copy org_id, billing_account, billing_project into: +# deploy/gcp/terraform/cloud-setup.auto.tfvars +# Loaded automatically by the installer into conf/prod/prole.cfg [GCP] + +# Org: knoey.com +org_id = "584001916389" + +# Billing account: My Billing Account +billing_account = "01193C-25783B-3211AD" + +# Billing project (used for API quota / billing attribution) +billing_project = "plenary-truck-485623-p7" + +# Selected project: Knoey Auth +project_id = "plenary-truck-485623-p7" diff --git a/conf/service/prole.cfg b/conf/service/prole.cfg index 25807ce..82830d6 100644 --- a/conf/service/prole.cfg +++ b/conf/service/prole.cfg @@ -1,5 +1,5 @@ ; Prole Master Configuration File -; Generated by install.py on 2026-03-31 18:38:57 +; Generated by install.py on 2026-04-01 00:50:45 ; This file is used as input for Ansible deployment and k8s cluster creation. [User] @@ -48,6 +48,7 @@ env_setup.PROLE_DATA = /Users/chrisfu/dev/prole/data env_setup.PROLE_HOME = /Users/chrisfu/dev/prole env_setup.PROLE_LOGS = /Users/chrisfu/dev/prole/logs env_setup.PROLE_SERVICE = /Users/chrisfu/dev/prole/etc +gitops.git_provider = Gitea gitops.node_selector = gandalf.prole.org init_cluster.argocd_enabled = false init_cluster.at_rest_encryption_enabled = true @@ -55,17 +56,11 @@ init_cluster.cluster_env = service init_cluster.deployment_target = prole-service-cluster init_cluster.gitops_enabled = true init_cluster.k3s_server_url = https://myrddin.prole.org:6443 -init_cluster.k3s_token = ${PROLE_SECRET:v1:m0esKgX4z-UxYI3V:Rtau88iOMq7zuzT4A9y5PnaWy80PrSqFmoo9vqqXAsOORqFIM1CdcDyFdkHxmIPKTsSEnj91LXmRr3mie83lq4Y-qRj1PMmDD5f3yAtvFA33i709FunDECChkJLVoZJrDHGnRuSaA5ZHwIvqNXRY36S4LxCKluHk6scy8A==} +init_cluster.k3s_token = ${PROLE_SECRET:v1:-5rud_XHzHRKoZIU:Dxv2nKbY883uDrJlmjRIqUKTtF42KPwXmzONy1s0-cIsJ2TLpTkaSWjfYx0tXNnz65lPnwIR16kXVtICYHhOxhfSIaz-lQiNGwZ_hm1tGMjp9BvpANt_l5Ie6swbOHVw8QfpGLtiXuIv7ur2ocSX4xAYSdCKtYWWV58U0Q==} init_cluster.kerberos_enabled = true init_cluster.mode = k3s init_cluster.start_cluster = true -init_cluster.supabase_analytics_enabled = true -init_cluster.supabase_auth_enabled = true -init_cluster.supabase_enabled = true -init_cluster.supabase_meta_enabled = true -init_cluster.supabase_realtime_enabled = true -init_cluster.supabase_studio_enabled = false -init_cluster.supabase_studio_url = db.prole.org +init_cluster.supabase_enabled = false init_cnpg_deploy.force_rollout = false init_cnpg_deploy.run_deploy = true init_db_build.run_build = true @@ -78,7 +73,6 @@ 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:U-EPMl7qv4heEB1k:BEcXGbI_LT4lCXwBuvFEGgPbLFa9MpVzECObdH0pbLtClDHf} kerberos_config.realm = PROLE.ORG @@ -107,7 +101,6 @@ DB_HOST_PORT = 5432 DB_PASSWORD = ${PROLE_SECRET:v1:t7NgRHfXTXH-mHcx:CRbKP9b6ccuxrTMMeJ742Q48_vegoUY4} DEPLOYMENT_MODE = k3s DEPLOYMENT_TARGET = prole-service-cluster -DOCKER_IMPORT_DIR = DOCKER_PRELOAD = false GITEA_NODE_SELECTOR = gandalf.prole.org K3S_SERVER = https://myrddin.prole.org:6443 @@ -116,9 +109,8 @@ KNOE_DB_USER = root KUBECONTEXT = knoe.dev.prole.org 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:-5rud_XHzHRKoZIU:Dxv2nKbY883uDrJlmjRIqUKTtF42KPwXmzONy1s0-cIsJ2TLpTkaSWjfYx0tXNnz65lPnwIR16kXVtICYHhOxhfSIaz-lQiNGwZ_hm1tGMjp9BvpANt_l5Ie6swbOHVw8QfpGLtiXuIv7ur2ocSX4xAYSdCKtYWWV58U0Q==} REGISTRY_NAMESPACE = knoe-system SERVICE_NAMESPACE = knoe-system SUPABASE_PV_BASE = /synology/d005 @@ -145,21 +137,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_14 = id=gitea-http;namespace=gitea;target=svc/gitea-http;address=0.0.0.0;hostPort=13000;servicePort=3000;protocol=TCP;description=Gitea Web -PORT_FORWARD_K3S_MAPPING_15 = id=gitea-ssh;namespace=gitea;target=svc/gitea-ssh;address=0.0.0.0;hostPort=22;servicePort=22;protocol=TCP;description=Gitea SSH -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 @@ -180,7 +158,7 @@ AD_TCP_PORTS = 88 389 445 464 636 AD_UDP_PORTS = 88 464 ENABLED = True KDC = 10.0.0.3 -PASSWORD = ${PROLE_SECRET:v1:jJ4tvCF8ef8xxQ-o:82chD7iVrlUNaDDA8K5DgYMWO2DN9FUSE7Rrm8IVhwuapK2P} +PASSWORD = ${PROLE_SECRET:v1:bQj9RtiWj7fnkThC:2lYvxy8AwfMwM17P23R3QchdYbueGw4FNzeTsWTjohJJNAX9} REALM = PROLE.ORG SERVER = 10.0.0.3 STATUS = Initialized @@ -193,14 +171,14 @@ OLLAMA_SERVER_PORT = 11434 [Optional Features] ARGOCD_ENABLED = False -AT_REST_ENCRYPTION_ENABLED = true -GITOPS_ENABLED = true +AT_REST_ENCRYPTION_ENABLED = True +GITOPS_ENABLED = True GITOPS_PROVIDER = gitea -KERBEROS_ENABLED = true -SUPABASE_ENABLED = true +KERBEROS_ENABLED = True +SUPABASE_ENABLED = False [GitOps] -STATUS = Deployed +; No configuration values captured yet for this section. [Database Creation] DB_USER = root @@ -208,12 +186,12 @@ DB_USER = root [Initialize Cluster] ENVIRONMENT = service K3S_SERVER_URL = https://myrddin.prole.org:6443 -K3S_TOKEN = ${PROLE_SECRET:v1:HAOwKPWbbBvJ7JUN:fuseXaNaQzGdzklTyEelMLldsma1MX7Ciq2P-nzrlsDXpJy9DXv4i4siVOa4aPIYvKP66fAh8IGfk_gwX0JwGQorSgGrgRpaEKjBtZ-0sh48AJPFkP7VkpQw4QZr5xzoDgAA96Q3r70TgDFHlo81OV8_riptpqYp5BO5nA==} +K3S_TOKEN = ${PROLE_SECRET:v1:-5rud_XHzHRKoZIU:Dxv2nKbY883uDrJlmjRIqUKTtF42KPwXmzONy1s0-cIsJ2TLpTkaSWjfYx0tXNnz65lPnwIR16kXVtICYHhOxhfSIaz-lQiNGwZ_hm1tGMjp9BvpANt_l5Ie6swbOHVw8QfpGLtiXuIv7ur2ocSX4xAYSdCKtYWWV58U0Q==} [Dev Cluster (k3d)] -CLUSTER_ENV = k3d-knoe-dev-cluster +CLUSTER_ENV = dev DISPLAY_NAME = knoe-dev-cluster -KUBECTL_CONTEXT = service +KUBECTL_CONTEXT = knoe.dev.prole.org MODE = k3d [Service Cluster (k3s)] @@ -222,17 +200,18 @@ DISPLAY_NAME = prole-service-cluster K3S_SERVER_URL = https://myrddin.prole.org:6443 K3S_TOKEN = ${PROLE_SECRET:v1:-5rud_XHzHRKoZIU:Dxv2nKbY883uDrJlmjRIqUKTtF42KPwXmzONy1s0-cIsJ2TLpTkaSWjfYx0tXNnz65lPnwIR16kXVtICYHhOxhfSIaz-lQiNGwZ_hm1tGMjp9BvpANt_l5Ie6swbOHVw8QfpGLtiXuIv7ur2ocSX4xAYSdCKtYWWV58U0Q==} MODE = k3s -PIPELINE_URL = http://myrddin.prole.org:8080 [GCP] -; No configuration values captured yet for this section. +BILLING_ACCOUNT = 01193C-25783B-3211AD +BILLING_PROJECT = plenary-truck-485623-p7 +ORG_ID = 584001916389 +PROJECT_ID = plenary-truck-485623-p7 [Prod Cluster (k8s)] -ARTIFACTS_DIR = +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 [Docker Build] ; No configuration values captured yet for this section. diff --git a/demo/gcp-config.cast b/demo/gcp-config.cast new file mode 100644 index 0000000..14e0b58 --- /dev/null +++ b/demo/gcp-config.cast @@ -0,0 +1,98 @@ +{"version":3,"term":{"cols":80,"rows":24,"type":"xterm-256color","theme":{"fg":"#ebebeb","bg":"#000000","palette":"#000000:#990000:#00a600:#999900:#0000b3:#b300b3:#00a6b3:#bfbfbf:#666666:#e60000:#00d900:#e6e600:#0000ff:#e600e6:#00e6e6:#e6e6e6"}},"timestamp":1775025588,"command":"./config.sh --mode k8s --provider gcp","env":{"SHELL":"/bin/bash"}} +[0.218, "o", "\u001b[?1049h\u001b[1;24r\u001b(B\u001b[m\u001b[4l\u001b[?7h\u001b[?1h\u001b="] +[0.000, "o", "\u001b[39;49m\u001b[?1h\u001b="] +[0.671, "o", "\u001b[?12l\u001b[?25h"] +[0.000, "o", "\u001b[39;49m\u001b(B\u001b[m\u001b[H\u001b[2J\u001b(B\u001b[0;1mGCP Authentication\r\n\u001b(B\u001b[m───────────────────────────────────────────────────────────────────────────────\r\n\n \u001b(B\u001b[0;2mEnter your Google account email and a valid access token.\n\u001b[3GTab to move between fields. Enter on the token field to continue.\n\n\u001b[3G\u001b(B\u001b[0;1mEmail: \n\n\u001b[3G\u001b(B\u001b[mAccess token:\r\u001b[24d\u001b(B\u001b[0;2mTab next field Enter submit Esc cancel\u001b(B\u001b[m"] +[3.258, "o", "\u001b[7;3HEmail:\u001b[K\r\n\n \u001b(B\u001b[0;1mAccess token:\u001b[24;43H\u001b(B\u001b[m"] +[0.483, "o", "\u001b[7;3H\u001b(B\u001b[0;1mEmail: \n\n\u001b[3G\u001b(B\u001b[mAccess token:\u001b[24;43H"] +[1.202, "o", "\u001b[7;17Hc\u001b[24;43H"] +[0.137, "o", "\u001b[7;18Hh\u001b[24;43H"] +[0.125, "o", "\u001b[7;19Hr\u001b[24;43H"] +[0.103, "o", "\u001b[7;20Hi\u001b[24;43H"] +[0.087, "o", "\u001b[7;21Hs\u001b[24;43H"] +[0.058, "o", "\u001b[7;22Hf\u001b[24;43H"] +[0.055, "o", "\u001b[7;23Hu\u001b[24;43H"] +[0.304, "o", "\u001b[7;24H@\u001b[24;43H"] +[0.729, "o", "\u001b[7;25Hk\u001b[24;43H"] +[0.232, "o", "\u001b[7;26Hn\u001b[24;43H"] +[0.236, "o", "\u001b[7;27Ho\u001b[24;43H"] +[0.335, "o", "\u001b[7;28He\u001b[24;43H"] +[0.109, "o", "\u001b[7;29Hy\u001b[24;43H"] +[0.342, "o", "\u001b[7;30H.\u001b[24;43H"] +[0.317, "o", "\u001b[7;31Hc\u001b[24;43H"] +[0.109, "o", "\u001b[7;32Ho\u001b[24;43H"] +[0.086, "o", "\u001b[7;33Hm\u001b[24;43H"] +[0.274, "o", "\u001b[7;3HEmail: \r\n\n \u001b(B\u001b[0;1mAccess token:\u001b[24;43H\u001b(B\u001b[m"] +[16.098, "o", "\u001b[9;17H*\u001b[24;43H"] +[0.000, "o", "\u001b[9;18H*\u001b[24;43H"] +[0.000, "o", "\u001b[9;19H*\u001b[24;43H"] +[0.000, "o", "\u001b[9;20H*\u001b[24;43H"] +[0.000, "o", "\u001b[9;21H*\u001b[24;43H"] +[0.000, "o", "\u001b[9;22H*\u001b[24;43H"] +[0.000, "o", "\u001b[9;23H*\u001b[24;43H"] +[0.000, "o", "\u001b[9;24H*\u001b[24;43H"] +[0.000, "o", "\u001b[9;25H*\u001b[24;43H"] +[0.000, "o", "\u001b[9;26H*\u001b[24;43H"] +[0.000, "o", "\u001b[9;27H*\u001b[24;43H"] +[0.000, "o", "\u001b[9;28H*\u001b[24;43H"] +[0.000, "o", "\u001b[9;29H*\u001b[24;43H"] +[0.000, "o", "\u001b[9;30H*\u001b[24;43H"] +[0.000, "o", "\u001b[9;31H*\u001b[24;43H"] +[0.000, "o", "\u001b[9;32H*\u001b[24;43H"] +[0.000, "o", "\u001b[9;33H*\u001b[24;43H"] +[0.000, "o", "\u001b[9;34H*\u001b[24;43H"] +[0.000, "o", "\u001b[9;35H*\u001b[24;43H"] +[0.000, "o", "\u001b[9;36H*\u001b[24;43H"] +[0.000, "o", "\u001b[9;37H*\u001b[24;43H"] +[0.000, "o", "\u001b[9;38H*\u001b[24;43H"] +[0.000, "o", "\u001b[9;39H*\u001b[24;43H\u001b[9;40H*\u001b[24;43H"] +[0.000, "o", "\u001b[9;41H*\u001b[24;43H"] +[0.000, "o", "\u001b[9;42H*\u001b[24;43H\u001b[9;43H*\u001b[24;43H"] +[0.000, "o", "\u001b[9;44H*\u001b[24;43H"] +[0.000, "o", "\u001b[9;45H*\u001b[24;43H"] +[0.000, "o", "\u001b[9;46H*\u001b[24;43H"] +[0.000, "o", "\u001b[9;47H*\u001b[24;43H\u001b[9;48H*\u001b[24;43H"] +[0.000, "o", "\u001b[9;49H*\u001b[24;43H"] +[0.000, "o", "\u001b[9;50H*\u001b[24;43H"] +[0.000, "o", "\u001b[9;51H*\u001b[24;43H"] +[0.001, "o", "\u001b[9;52H*\u001b[24;43H"] +[0.000, "o", "\u001b[9;53H*\u001b[24;43H"] +[0.000, "o", "\u001b[9;54H*\u001b[24;43H"] +[0.000, "o", "\u001b[9;55H*\u001b[24;43H"] +[0.000, "o", "\u001b[9;56H*\u001b[24;43H"] +[0.000, "o", "\u001b[9;57H*\u001b[24;43H"] +[0.000, "o", "\u001b[9;58H*\u001b[24;43H"] +[0.000, "o", "\u001b[9;59H*\u001b[24;43H"] +[0.000, "o", "\u001b[9;60H*\u001b[24;43H"] +[0.000, "o", "\u001b[9;61H*\u001b[24;43H"] +[0.000, "o", "\u001b[9;62H*\u001b[24;43H"] +[0.000, "o", "\u001b[9;63H*\u001b[24;43H"] +[0.000, "o", "\u001b[9;64H*\u001b[24;43H\u001b[9;65H*\u001b[24;43H"] +[0.000, "o", "\u001b[9;66H*\u001b[24;43H"] +[0.000, "o", "\u001b[9;67H*\u001b[24;43H\u001b[9;68H*\u001b[24;43H"] +[0.000, "o", "\u001b[9;69H*\u001b[24;43H"] +[0.000, "o", "\u001b[9;70H*\u001b[24;43H"] +[0.000, "o", "\u001b[9;71H*\u001b[24;43H"] +[0.000, "o", "\u001b[9;72H*\u001b[24;43H"] +[0.000, "o", "\u001b[9;73H*\u001b[24;43H"] +[0.000, "o", "\u001b[9;74H*\u001b[24;43H"] +[0.000, "o", "\u001b[9;75H*\u001b[24;43H"] +[0.000, "o", "\u001b[9;76H*\u001b[24;43H"] +[0.000, "o", "\u001b[9;77H*\u001b[24;43H"] +[0.000, "o", "\u001b[9;78H*\u001b[24;43H"] +[1.231, "o", "\u001b[?25l"] +[0.000, "o", "\r\u001b[4d\u001b[J\u001b[HAuthenticated as: chrisfu@knoey.com\r\n\u001b[K\nFetching GCP organizations…"] +[1.056, "o", "\u001b[H\u001b(B\u001b[0;1mSelect GCP Organization\u001b(B\u001b[m\u001b[K\r\n───────────────────────────────────────────────────────────────────────────────\r\n \u001b(B\u001b[0;2mAuthenticated as: chrisfu@knoey.com\n\u001b[3G\u001b(B\u001b[0;7m knoey.com 584001916389 \r\u001b[24d\u001b(B\u001b[0;2m↑↓/jk navigate Enter select q quit 1/1\u001b(B\u001b[m"] +[2.787, "o", "\u001b[H\n\u001b[J\u001b[AFetching projects…\u001b[K"] +[1.020, "o", "\r\u001b(B\u001b[0;1mSelect GCP Project\r\n\u001b(B\u001b[m───────────────────────────────────────────────────────────────────────────────\r\n \u001b(B\u001b[0;2mOrg: knoey.com (2 projects)\n\u001b[3G\u001b(B\u001b[0;7m Knoey Auth plenary-truck-485623-p7 #507759 \u001b[5;3H\u001b(B\u001b[mTest New Project\u001b[5;40Htest-new-project-491616\u001b[5;72H#826674\r\u001b[24d\u001b(B\u001b[0;2m↑↓/jk navigate Enter select q quit 1/2\u001b(B\u001b[m"] +[1.171, "o", "\u001b[4;3HKnoey Auth\u001b[27X\u001b[4;40Hplenary-truck-485623-p7 #507759\u001b[K\r\n \u001b(B\u001b[0;7m Test New Project test-new-project-491616 #826674 \u001b[24;41H\u001b(B\u001b[0;2m2/2\u001b(B\u001b[m"] +[0.549, "o", "\u001b[4;3H\u001b(B\u001b[0;7m Knoey Auth plenary-truck-485623-p7 #507759 \u001b[5;3H\u001b(B\u001b[mTest New Project\u001b[21X\u001b[5;40Htest-new-project-491616 #826674\u001b[K\u001b[24;41H\u001b(B\u001b[0;2m1/2\u001b(B\u001b[m"] +[0.672, "o", "\u001b[H\n\u001b[J\u001b[AFetching billing accounts…"] +[31.063, "o", "\r\u001b(B\u001b[0;1mSelect Billing Account\u001b(B\u001b[m\u001b[K\r\n───────────────────────────────────────────────────────────────────────────────\r\n \u001b(B\u001b[0;2mProject: plenary-truck-485623-p7\n\u001b[3G\u001b(B\u001b[0;7m My Billing Account 01193C-25783B-3211AD \r\u001b[24d\u001b(B\u001b[0;2m↑↓/jk navigate Enter select q quit 1/1\u001b(B\u001b[m"] +[2.113, "o", "\u001b[?12l\u001b[?25h"] +[0.001, "o", "\u001b[H\u001b(B\u001b[0;1mConfig: --mode k8s --provider gcp — Confirm & Save\n\n\u001b[3G\u001b(B\u001b[mOrg ID: 584001916389\u001b[K\r\n Org Name: knoey.com\u001b[K\r\n Project ID:\u001b[5;22Hplenary-truck-485623-p7\r\n Project Name:\u001b[22GKnoey Auth\r\n Project Number: 507759242125\r\n Billing Account: 01193C-25783B-3211AD\r\n Billing Name:\u001b[22GMy Billing Account\r\n\n \u001b(B\u001b[0;1mOutput file: \u001b(B\u001b[m/Users/chrisfu/dev/prole/conf/prod/gcp.cfg\r\u001b[24d\u001b(B\u001b[0;2mEdit path above Enter to write Esc to cancel\u001b(B\u001b[m"] +[4.691, "o", "\u001b[?25l"] +[0.002, "o", "\u001b[HConfiguration written.\u001b[K\r\n\u001b[K\n /Users/chrisfu/dev/prole/conf/prod/gcp.cfg\r\n\u001b[K\nNext steps:\u001b[K\r\n 1. Copy org_id / billing_account / billing_project into:\n\u001b[7G\u001b[1K deploy/gcp/terraform/cloud-setup.auto.tfvars\r\n 2. Open the installer — Prod Cluster → Cloud tab will be pre-filled\r\n (or click 'Load from gcp.cfg' to refresh on demand)\r\n\n\u001b[K\u001b[24d\u001b(B\u001b[0;2mPress any key to continue…\u001b(B\u001b[m\u001b[K"] +[2.724, "o", "\u001b[?1l\u001b>"] +[0.000, "o", "\u001b[24;1H\u001b[?12l\u001b[?25h\u001b[?1049l\r\u001b[?1l\u001b>"] +[0.011, "x", "0"] diff --git a/demo/gcp-config.gif b/demo/gcp-config.gif new file mode 100644 index 0000000..701af99 Binary files /dev/null and b/demo/gcp-config.gif differ diff --git a/etc/init_gitlab.sh b/etc/init_gitlab.sh index a4c5216..70dcdc4 100644 --- a/etc/init_gitlab.sh +++ b/etc/init_gitlab.sh @@ -11,6 +11,7 @@ MODE="$(prole_normalize_mode "${PROLE_MODE:-${DEPLOYMENT_MODE:-k3d}}")" NAMESPACE="${GITLAB_NAMESPACE:-}" CFG_PATH="" FORCE=0 +NODE_SELECTOR="${GITLAB_NODE_SELECTOR:-${NODE_SELECTOR:-gandalf.prole.org}}" usage() { cat < Deployment mode (default: ${MODE:-k3d}) - -n, --namespace Target namespace (default: gitlab) - -c, --config Path to prole.cfg (defaults to detected) - --force Uninstall existing GitLab release before deploy - --help Show this help + -n, --namespace Target namespace (default: gitlab) + -c, --config Path to prole.cfg (defaults to detected) + --node-selector Node to pin GitLab workloads (default: gandalf.prole.org) + --force Remove existing GitLab and Gitea releases before deploy + --help Show this help Behavior: - - Installs self-hosted GitLab via the official Helm chart. - - Uses CloudNativePG (CNPG) Postgres as the database backend (external DB). + - Removes any pre-existing git.prole.org Gitea configurations (helm release, + namespace, Kong routes) when --force is specified or when gitea is detected. + - Installs the GitLab Operator via Helm into the gitlab namespace. + - Provisions a GitLab CR that uses the knoe-db CloudNativePG cluster as its + external PostgreSQL data store. + - Configures git.prole.org as a Kong-managed ingress pointing to GitLab. + - Pins all GitLab workloads to the selected node (default: gandalf.prole.org). EOF } -die() { echo "[ERROR] $*" >&2; exit 2; } -log() { echo "[INFO] $*"; } -warn() { echo "[WARN] $*" >&2; } +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 1 ;; - -h|--help) usage; exit 0 ;; + --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 ;; + --node-selector) NODE_SELECTOR="${2:-}"; shift 2 ;; + --node-selector=*) NODE_SELECTOR="${1#*=}"; shift 1 ;; + --force) FORCE=1; shift 1 ;; + -h|--help) usage; exit 0 ;; *) break ;; esac done -# Resolve config path and namespace defaults from prole.cfg when present +# --------------------------------------------------------------------------- +# Resolve config path +# --------------------------------------------------------------------------- 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 @@ -56,8 +67,8 @@ elif [[ -z "$CFG_PATH" && -f "$SCRIPT_DIR/../conf/prole.cfg" ]]; then 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" "GITLAB_NAMESPACE")" + maybe_ns="$(_prole_cfg_extract_key "$CFG_PATH" "GITLAB_NAMESPACE")" + [[ -z "$maybe_ns" ]] && maybe_ns="$(_prole_cfg_extract_key "$CFG_PATH" "GITOPS_NAMESPACE")" NAMESPACE="$maybe_ns" fi NAMESPACE="${NAMESPACE:-gitlab}" @@ -70,105 +81,375 @@ esac export PROLE_MODE="$MODE" command -v kubectl >/dev/null || die "kubectl not found" -command -v helm >/dev/null || die "helm not found (required for GitLab install)" +command -v helm >/dev/null || die "helm not found (required for GitLab Operator install)" -DB_NAMESPACE="${PROLE_NAMESPACE:-}" -if [[ -z "$DB_NAMESPACE" && -n "$CFG_PATH" ]]; then - DB_NAMESPACE="$(_prole_cfg_extract_key "$CFG_PATH" "NAMESPACE")" -fi -DB_NAMESPACE="${DB_NAMESPACE:-default}" - -CNPG_CLUSTER_NAME="${CNPG_CLUSTER_NAME:-knoe-db}" +# --------------------------------------------------------------------------- +# knoe-db (CNPG) settings — reuse the same cluster gitea uses +# --------------------------------------------------------------------------- +DB_NAMESPACE="${KNOE_DB_NAMESPACE:-${DATABASE_NAMESPACE:-knoe-db}}" +CNPG_CLUSTER_NAME="${CNPG_CLUSTER_NAME:-${CLUSTER_NAME:-knoe-db}}" GITLAB_DB_NAME="${GITLAB_DB_NAME:-gitlabhq_production}" GITLAB_DB_USER="${GITLAB_DB_USER:-gitlab}" GITLAB_DB_PASSWORD="${GITLAB_DB_PASSWORD:-}" -if [[ -z "$GITLAB_DB_PASSWORD" ]]; then - GITLAB_DB_PASSWORD=$(LC_ALL=C tr -dc 'A-Za-z0-9' /dev/null | head -c 32 || true) -fi - DB_HOST="${CNPG_CLUSTER_NAME}-rw.${DB_NAMESPACE}.svc.cluster.local" DB_PORT="${GITLAB_DB_PORT:-5432}" -kubectl get ns "$NAMESPACE" >/dev/null 2>&1 || kubectl create namespace "$NAMESPACE" >/dev/null 2>&1 +# GitLab domain (reuses the git.prole.org shared hostname) +GITLAB_DOMAIN="${GITLAB_DOMAIN:-git.prole.org}" +GITLAB_RELEASE="gitlab" -ensure_db() { - local primary_pod="" - primary_pod=$(kubectl -n "$DB_NAMESPACE" get pods \ - -l "cnpg.io/cluster=${CNPG_CLUSTER_NAME},role=primary" \ +# --------------------------------------------------------------------------- +# Operator chart coordinates +# --------------------------------------------------------------------------- +OPERATOR_REPO_NAME="gitlab-operator" +OPERATOR_REPO_URL="https://gitlab.com/api/v4/projects/18899486/packages/helm/stable" +OPERATOR_CHART="gitlab-operator/gitlab-operator" +OPERATOR_RELEASE="gitlab-operator" +OPERATOR_NAMESPACE="${GITLAB_OPERATOR_NAMESPACE:-${NAMESPACE}}" +# GitLab chart version pinned inside the CR; leave empty to use operator default +GITLAB_CHART_VERSION="${GITLAB_CHART_VERSION:-}" + +# --------------------------------------------------------------------------- +# Helper: resolve knoe-db primary pod (same pattern as init_gitea.sh) +# --------------------------------------------------------------------------- +resolve_knoe_db_primary_pod() { + local primary + primary=$(kubectl -n "$DB_NAMESPACE" get pods \ + -l "cnpg.io/cluster=${CNPG_CLUSTER_NAME},cnpg.io/instanceRole=primary" \ -o jsonpath='{.items[0].metadata.name}' 2>/dev/null || true) - if [[ -z "$primary_pod" ]]; then - primary_pod=$(kubectl -n "$DB_NAMESPACE" get pods \ + if [[ -z "$primary" ]]; then + primary=$(kubectl -n "$DB_NAMESPACE" get pods \ -l "cnpg.io/cluster=${CNPG_CLUSTER_NAME}" \ -o jsonpath='{.items[0].metadata.name}' 2>/dev/null || true) fi - if [[ -z "$primary_pod" ]]; then - die "Could not find CNPG pod for cluster '${CNPG_CLUSTER_NAME}' in namespace '${DB_NAMESPACE}'" - fi - - log "Ensuring database '${GITLAB_DB_NAME}' and role '${GITLAB_DB_USER}' exist in CNPG cluster '${CNPG_CLUSTER_NAME}' (ns=${DB_NAMESPACE})" - - kubectl -n "$DB_NAMESPACE" exec "$primary_pod" -- bash -lc "psql -v ON_ERROR_STOP=1 -U postgres -d postgres" </dev/null + printf '%s' "$primary" } -ensure_db +sql_escape_literal() { + printf '%s' "${1:-}" | sed "s/'/''/g" +} -RELEASE_NAME="${GITLAB_RELEASE_NAME:-gitlab}" +# --------------------------------------------------------------------------- +# Resolve GitLab DB password (pull from knoe-db-superuser secret as fallback) +# --------------------------------------------------------------------------- +resolve_gitlab_db_password() { + if [[ -n "${GITLAB_DB_PASSWORD:-}" ]]; then + return 0 + fi + local resolved="" + if kubectl -n "$DB_NAMESPACE" get secret knoe-db-superuser >/dev/null 2>&1; then + resolved=$(kubectl -n "$DB_NAMESPACE" get secret knoe-db-superuser \ + -o jsonpath='{.data.password}' 2>/dev/null | base64 -d 2>/dev/null || true) + fi + if [[ -n "$resolved" ]]; then + GITLAB_DB_PASSWORD="$resolved" + else + # Generate a random password when none is available + GITLAB_DB_PASSWORD="$(LC_ALL=C tr -dc 'A-Za-z0-9' /dev/null | head -c 32 || true)" + warn "Generated random GitLab DB password; store it in GITLAB_DB_PASSWORD for future runs." + fi +} +# --------------------------------------------------------------------------- +# Provision GitLab role + database in knoe-db +# --------------------------------------------------------------------------- +setup_knoe_db_for_gitlab() { + local primary + primary="$(resolve_knoe_db_primary_pod)" + if [[ -z "$primary" ]]; then + warn "No knoe-db pod found in namespace '${DB_NAMESPACE}'; skipping GitLab DB setup." + return 0 + fi + + resolve_gitlab_db_password + + local admin_user="" + local candidate + for candidate in postgres root; do + if kubectl -n "$DB_NAMESPACE" exec "$primary" -c postgres -- \ + psql -U "$candidate" -d postgres -tAc "SELECT 1" >/dev/null 2>&1; then + admin_user="$candidate" + break + fi + done + + if [[ -z "$admin_user" ]]; then + warn "Unable to connect to knoe-db as admin; skipping GitLab DB setup." + return 0 + fi + + local esc_pw + esc_pw="$(sql_escape_literal "$GITLAB_DB_PASSWORD")" + + kubectl -n "$DB_NAMESPACE" exec "$primary" -c postgres -- \ + psql -U "$admin_user" -d postgres -c " + DO \$\$ BEGIN + IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname='${GITLAB_DB_USER}') THEN + CREATE ROLE ${GITLAB_DB_USER} LOGIN PASSWORD '${esc_pw}'; + ELSE + ALTER ROLE ${GITLAB_DB_USER} WITH PASSWORD '${esc_pw}'; + END IF; + END \$\$; + " >/dev/null 2>&1 || warn "Could not create/update role '${GITLAB_DB_USER}'." + + local db_exists + db_exists=$(kubectl -n "$DB_NAMESPACE" exec "$primary" -c postgres -- \ + psql -U "$admin_user" -d postgres -tAc \ + "SELECT 1 FROM pg_database WHERE datname='${GITLAB_DB_NAME}';" 2>/dev/null || true) + + if [[ "$db_exists" != "1" ]]; then + kubectl -n "$DB_NAMESPACE" exec "$primary" -c postgres -- \ + psql -U "$admin_user" -d postgres -c \ + "CREATE DATABASE ${GITLAB_DB_NAME} OWNER ${GITLAB_DB_USER};" \ + >/dev/null 2>&1 || warn "Could not create database '${GITLAB_DB_NAME}'." + fi + + log "GitLab database '${GITLAB_DB_NAME}' prepared in knoe-db (ns=${DB_NAMESPACE})." +} + +# --------------------------------------------------------------------------- +# Clean up any pre-existing Gitea / git.prole.org configurations +# --------------------------------------------------------------------------- +cleanup_gitea() { + log "Checking for pre-existing Gitea deployment to remove before GitLab install..." + + local gitea_ns="${GITEA_NAMESPACE:-gitea}" + + # Remove Gitea Helm release + if helm -n "$gitea_ns" status gitea >/dev/null 2>&1; then + log "Uninstalling Gitea Helm release from namespace '$gitea_ns'..." + helm -n "$gitea_ns" uninstall gitea >/dev/null 2>&1 || true + fi + + # Remove any leftover raw Gitea resources + kubectl -n "$gitea_ns" delete deploy/gitea svc/gitea-http svc/gitea-ssh \ + >/dev/null 2>&1 || true + + # Remove Kong routes/services registered for git.prole.org (best-effort) + local kong_ns="${KONG_NAMESPACE:-${NAMESPACE:-kong}}" + for res_type in kongplugins kongingresses; do + kubectl -n "$gitea_ns" delete "$res_type" --all >/dev/null 2>&1 || true + done + + # Remove gitea namespace Ingress objects that route git.prole.org + kubectl -n "$gitea_ns" delete ingress \ + -l "app.kubernetes.io/name=gitea" >/dev/null 2>&1 || true + kubectl -n "$gitea_ns" delete ingress \ + --field-selector="metadata.name=gitea" >/dev/null 2>&1 || true + + log "Gitea cleanup complete." +} + +# --------------------------------------------------------------------------- +# Ensure gitlab namespace exists +# --------------------------------------------------------------------------- +kubectl get ns "$NAMESPACE" >/dev/null 2>&1 || kubectl create namespace "$NAMESPACE" >/dev/null + +# --------------------------------------------------------------------------- +# --force: remove existing GitLab operator and release first +# --------------------------------------------------------------------------- if [[ "$FORCE" -eq 1 ]]; then - warn "--force specified; uninstalling existing release '$RELEASE_NAME' in namespace '$NAMESPACE' (if present)" - helm -n "$NAMESPACE" uninstall "$RELEASE_NAME" >/dev/null 2>&1 || true + warn "--force: removing existing GitLab resources in namespace '$NAMESPACE'..." + if kubectl get crd gitlabs.apps.gitlab.com >/dev/null 2>&1; then + kubectl -n "$NAMESPACE" delete gitlab "$GITLAB_RELEASE" >/dev/null 2>&1 || true + # Wait briefly for operator to clean up managed resources + sleep 10 + fi + helm -n "$NAMESPACE" uninstall "$GITLAB_RELEASE" >/dev/null 2>&1 || true + helm -n "$NAMESPACE" uninstall "$OPERATOR_RELEASE" >/dev/null 2>&1 || true + cleanup_gitea fi -GITLAB_DOMAIN="${GITLAB_DOMAIN:-${DOMAIN:-${PROLE_DOMAIN:-}}}" -GITLAB_DOMAIN="${GITLAB_DOMAIN:-example.local}" +# --------------------------------------------------------------------------- +# Always remove gitea if its helm release exists (non-destructive path) +# gitea and gitlab both claim git.prole.org; they cannot coexist. +# --------------------------------------------------------------------------- +if helm -n "${GITEA_NAMESPACE:-gitea}" status gitea >/dev/null 2>&1; then + warn "Gitea release detected — removing to free git.prole.org for GitLab..." + cleanup_gitea +fi -log "Adding/updating GitLab Helm repo" -helm repo add gitlab https://charts.gitlab.io/ >/dev/null 2>&1 || true -helm repo update >/dev/null +# --------------------------------------------------------------------------- +# Prepare knoe-db +# --------------------------------------------------------------------------- +setup_knoe_db_for_gitlab -log "Installing GitLab (release=$RELEASE_NAME, ns=$NAMESPACE, domain=$GITLAB_DOMAIN)" +# Persist the DB password as a k8s Secret the operator CR can reference +resolve_gitlab_db_password +kubectl -n "$NAMESPACE" create secret generic gitlab-db-password \ + --from-literal=password="$GITLAB_DB_PASSWORD" \ + --dry-run=client -o yaml | kubectl apply -f - >/dev/null +log "gitlab-db-password secret applied in namespace '$NAMESPACE'." -# Notes: -# - We keep the chart install minimal to avoid assuming ingress/cert-manager setup. -# - We disable the bundled PostgreSQL and point GitLab to CNPG. -helm upgrade --install "$RELEASE_NAME" gitlab/gitlab \ - -n "$NAMESPACE" \ - --timeout 30m \ +# --------------------------------------------------------------------------- +# Install / upgrade the GitLab Operator +# --------------------------------------------------------------------------- +log "Adding/updating GitLab Operator Helm repo..." +helm repo add "$OPERATOR_REPO_NAME" "$OPERATOR_REPO_URL" >/dev/null 2>&1 || true +helm repo update >/dev/null 2>&1 + +log "Installing GitLab Operator (release=${OPERATOR_RELEASE}, ns=${OPERATOR_NAMESPACE})..." + +# The operator needs cluster-scoped RBAC; it watches all namespaces by default. +helm upgrade --install "$OPERATOR_RELEASE" "$OPERATOR_CHART" \ + -n "$OPERATOR_NAMESPACE" \ + --create-namespace \ + --timeout 10m \ --wait \ - --set global.hosts.domain="$GITLAB_DOMAIN" \ - --set global.hosts.https=false \ - --set certmanager.install=false \ - --set prometheus.install=false \ - --set postgresql.install=false \ - --set global.psql.host="$DB_HOST" \ - --set global.psql.port="$DB_PORT" \ - --set global.psql.username="$GITLAB_DB_USER" \ - --set global.psql.database="$GITLAB_DB_NAME" \ - --set global.psql.password.secret=gitlab-db \ - --set global.psql.password.key=password + --set watchNamespace="$NAMESPACE" -log "Done. Inspect services/pods with: kubectl -n $NAMESPACE get pods,svc" +log "GitLab Operator ready." + +# --------------------------------------------------------------------------- +# Determine node selector block for the GitLab CR +# --------------------------------------------------------------------------- +NODE_SELECTOR_YAML="" +if [[ -n "$NODE_SELECTOR" ]]; then + log "Pinning GitLab workloads to node: ${NODE_SELECTOR}" + NODE_SELECTOR_YAML="kubernetes.io/hostname: ${NODE_SELECTOR}" +fi + +# --------------------------------------------------------------------------- +# Optional chart version pin inside the CR +# --------------------------------------------------------------------------- +CHART_VERSION_YAML="" +if [[ -n "$GITLAB_CHART_VERSION" ]]; then + CHART_VERSION_YAML="version: \"${GITLAB_CHART_VERSION}\"" +fi + +# --------------------------------------------------------------------------- +# Create the GitLab CR (operator reconciles this into the full deployment) +# --------------------------------------------------------------------------- +log "Applying GitLab CR (domain=${GITLAB_DOMAIN}, db=${GITLAB_DB_NAME}@${DB_HOST})..." + +kubectl apply -f - </dev/null; then + warn "Timed out waiting for GitLab CR condition=Available." + warn "The deployment may still be in progress. Check: kubectl -n ${NAMESPACE} describe gitlab ${GITLAB_RELEASE}" + fi +fi + +# --------------------------------------------------------------------------- +# Kong ingress for git.prole.org -> gitlab-webservice +# The GitLab Operator creates the Ingress; this block patches it to Kong class +# or creates a fallback Kong-native ingress if the CR-managed one is absent. +# --------------------------------------------------------------------------- +WEBSERVICE_SVC="${GITLAB_RELEASE}-webservice-default" + +log "Ensuring Kong ingress for ${GITLAB_DOMAIN} -> ${WEBSERVICE_SVC}:8181 ..." + +kubectl apply -f - < Bootstrapping Prole installer..." + command -v git >/dev/null 2>&1 || { echo "Error: git is required." >&2; exit 1; } + command -v python3 >/dev/null 2>&1 || { echo "Error: python3 is required." >&2; exit 1; } + + if [[ -d "${_INSTALL_DIR}/.git" ]]; then + echo "==> Updating existing checkout at ${_INSTALL_DIR} ..." + git -C "${_INSTALL_DIR}" pull --ff-only + else + echo "==> Cloning to ${_INSTALL_DIR} ..." + git clone "${PROLE_REPO_URL}" "${_INSTALL_DIR}" + fi + + exec "${_INSTALL_DIR}/install.sh" "$@" +} + +# Resolve script directory; empty when running via curl pipe. +_src="${BASH_SOURCE[0]:-}" +_script_dir="$(cd "$(dirname "${_src:-/}")" 2>/dev/null && pwd || echo "")" + +if [[ -n "${_script_dir}" && -d "${_script_dir}/knoe" ]]; then + # Local checkout: delegate to the Python installer module. + cd "${_script_dir}" + exec python3 -m knoe.ui.screens "$@" +else + # No local checkout found — bootstrap from the remote repo. + _bootstrap "$@" +fi diff --git a/knoe/ncurses_installer.py b/knoe/ncurses_installer.py index 41736e0..2567784 100644 --- a/knoe/ncurses_installer.py +++ b/knoe/ncurses_installer.py @@ -223,7 +223,7 @@ class KnoeNcursesInstaller: def _render_header(self): """Render the header.""" try: - title = "Prole Database Installer" + title = "Knoe System Installer" self.header_win.addstr(0, 2, title, curses.A_BOLD) self.header_win.addstr(1, 2, "─" * (self.width - 4)) if self.status_message: @@ -686,7 +686,7 @@ class KnoeNcursesInstaller: def _render_welcome(self): """Render the welcome screen.""" win = CursesWindow(self.main_content_win) - win.render_title("Welcome to Prole Database Installer", y=2) + win.render_title("Welcome to Knoe System Installer", y=2) win.render_paragraph( "This installer will guide you through setting up the Prole database system, " "including Docker containers, dependencies, and deployment configuration.", diff --git a/knoe/ui/screens/__init__.py b/knoe/ui/screens/__init__.py index 1e5dc7e..41c8779 100644 --- a/knoe/ui/screens/__init__.py +++ b/knoe/ui/screens/__init__.py @@ -38,7 +38,7 @@ if platform.system() == "Darwin": try: from Foundation import NSProcessInfo - NSProcessInfo.processInfo().setProcessName_("Prole Database Installer") + NSProcessInfo.processInfo().setProcessName_("Knoe System Installer") except Exception: pass @@ -211,7 +211,7 @@ class KnoeInstaller( self._reset_cluster = bool(reset_cluster) _ensure_ansible_vault_credentials(prompt_ui=True, root=root) self._action_flags = {} - self.root.title("Prole Database Installer") + self.root.title("Knoe System Installer") try: self._set_app_identity() except Exception: @@ -331,9 +331,8 @@ class KnoeInstaller( ("Database Creation", "init_password"), ("Initialization Scripts", "init_scripts"), ("Kerberos Authentication", "kerberos_config"), - ("GitOps Provider", "gitops_choice"), ("ArgoCD", "argocd_config"), - ("GitOps / Gitea", "gitops_config"), + ("GitOps", "gitops_config"), ("Supabase", "supabase_config"), ("Deployment", "init_cnpg_deploy"), ("Post Install", "create_installer"), @@ -415,6 +414,7 @@ class KnoeInstaller( self.gitops_enabled = tk.BooleanVar(value=False) self.gitops_namespace = tk.StringVar(value="gitea") self.gitops_node_selector = tk.StringVar(value="") + self.gitops_git_provider = tk.StringVar(value="Gitea") self.argocd_enabled = tk.BooleanVar(value=False) self.argocd_namespace = tk.StringVar(value="argocd") self.argocd_node_selector = tk.StringVar(value="") @@ -830,7 +830,6 @@ class KnoeInstaller( 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) @@ -869,7 +868,6 @@ class KnoeInstaller( self._register_page("init_password", 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) @@ -910,7 +908,7 @@ def has_display(): def main(): import argparse - parser = argparse.ArgumentParser(description="Prole Database Installer") + parser = argparse.ArgumentParser(description="Knoe System Installer") parser.add_argument( "-c", "--config", @@ -1048,7 +1046,7 @@ def main(): if use_gui: try: root = tk.Tk() - root.title("Prole Database Installer") + root.title("Knoe System Installer") window_width, window_height = 1000, 700 try: sw, sh = root.winfo_screenwidth(), root.winfo_screenheight() diff --git a/knoe/ui/screens/__main__.py b/knoe/ui/screens/__main__.py new file mode 100644 index 0000000..f75744a --- /dev/null +++ b/knoe/ui/screens/__main__.py @@ -0,0 +1,3 @@ +from knoe.ui.screens import main + +main() diff --git a/knoe/ui/screens/argocd.py b/knoe/ui/screens/argocd.py index 5340c34..857d5a2 100644 --- a/knoe/ui/screens/argocd.py +++ b/knoe/ui/screens/argocd.py @@ -25,7 +25,7 @@ class ArgoCDScreenMixin: self, right_margin, 40, - "Prole", + "knoe.dev", fill="#6e6e73", font=("SF Pro Text", 32, "bold"), anchor="ne", @@ -34,7 +34,7 @@ class ArgoCDScreenMixin: self, right_margin, 85, - "Infrastructure Automated.", + "infrastructure.auto()", fill="#6e6e73", font=("SF Pro Text", 18), anchor="ne", diff --git a/knoe/ui/screens/base.py b/knoe/ui/screens/base.py index a47f09c..beebc70 100644 --- a/knoe/ui/screens/base.py +++ b/knoe/ui/screens/base.py @@ -110,7 +110,7 @@ class ScreenBaseMixin: x_options=190, option_spacing=175, ): - """Render a reusable k3s node selector and return the next y offset.""" + """Render a reusable k3s node selector dropdown and return the next y offset.""" if not self._is_k3s_mode_active(): return y @@ -124,35 +124,26 @@ class ScreenBaseMixin: ) self._canvas_items.append(item) - selected = (target_var.get() or "").strip() nodes = self._fetch_k3s_nodes() - x = x_options - y_radio = y - 2 - for node in nodes: - disabled = self._is_tainted_node(node) - rb = tk.Radiobutton( - self.bg_canvas, - text=node, - variable=target_var, - value=node, - bg="white", - fg="#8c8c8c" if disabled else "black", - activebackground="white", - selectcolor="white", - state="disabled" if disabled else "normal", - font=("SF Pro Text", 10), - ) - window = self.bg_canvas.create_window(x, y_radio, window=rb, anchor="nw") - self._canvas_items.append(window) - self._overlay_widgets.append(rb) - x += option_spacing - - if (not selected or selected not in nodes or self._is_tainted_node(selected)) and nodes: + selected = (target_var.get() or "").strip() + if (not selected or selected not in nodes) and nodes: for node in nodes: if not self._is_tainted_node(node): target_var.set(node) break + combo = ttk.Combobox( + self.bg_canvas, + textvariable=target_var, + values=nodes, + state="readonly", + font=("SF Pro Text", 11), + width=30, + ) + window = self.bg_canvas.create_window(x_options, y - 2, window=combo, anchor="nw") + self._canvas_items.append(window) + self._overlay_widgets.append(combo) + return y + 36 def safe_after(self, func, delay=0): diff --git a/knoe/ui/screens/build.py b/knoe/ui/screens/build.py index ca9b973..b1f8982 100644 --- a/knoe/ui/screens/build.py +++ b/knoe/ui/screens/build.py @@ -30,7 +30,7 @@ class BuildScreenMixin: self, right_margin, 40, - "Prole", + "knoe.dev", fill="#6e6e73", font=("SF Pro Text", 32, "bold"), anchor="ne", @@ -39,7 +39,7 @@ class BuildScreenMixin: self, right_margin, 85, - "Infrastructure Automated.", + "infrastructure.auto()", fill="#6e6e73", font=("SF Pro Text", 18), anchor="ne", diff --git a/knoe/ui/screens/cfg.py b/knoe/ui/screens/cfg.py index 0a2e4cd..1ed51ef 100644 --- a/knoe/ui/screens/cfg.py +++ b/knoe/ui/screens/cfg.py @@ -537,6 +537,7 @@ class ConfigMixin: # GitOps _set("gitops.namespace", _get_var(self.gitops_namespace, "gitea")) _set("gitops.node_selector", _get_var(self.gitops_node_selector, "")) + _set("gitops.git_provider", _get_var(self.gitops_git_provider, "Gitea")) # ArgoCD _set("argocd.namespace", _get_var(self.argocd_namespace, "argocd")) diff --git a/knoe/ui/screens/cluster.py b/knoe/ui/screens/cluster.py index c57e615..eb1d355 100644 --- a/knoe/ui/screens/cluster.py +++ b/knoe/ui/screens/cluster.py @@ -49,7 +49,7 @@ class ClusterScreenMixin: self, right_margin, 40, - "Prole", + "knoe.dev", fill="#6e6e73", font=("SF Pro Text", 32, "bold"), anchor="ne", @@ -58,7 +58,7 @@ class ClusterScreenMixin: self, right_margin, 85, - "Infrastructure Automated.", + "infrastructure.auto()", fill="#6e6e73", font=("SF Pro Text", 18), anchor="ne", @@ -608,7 +608,7 @@ class ClusterScreenMixin: self, right_margin, 40, - "Prole", + "knoe.dev", fill="#6e6e73", font=("SF Pro Text", 32, "bold"), anchor="ne", @@ -617,7 +617,7 @@ class ClusterScreenMixin: self, right_margin, 85, - "Infrastructure Automated.", + "infrastructure.auto()", fill="#6e6e73", font=("SF Pro Text", 18), anchor="ne", diff --git a/knoe/ui/screens/database.py b/knoe/ui/screens/database.py index 5c99b35..378b0fd 100644 --- a/knoe/ui/screens/database.py +++ b/knoe/ui/screens/database.py @@ -37,7 +37,7 @@ class DatabaseScreenMixin: self, right_margin, 40, - "Prole", + "knoe.dev", fill="#6e6e73", font=("SF Pro Text", 32, "bold"), anchor="ne", @@ -46,7 +46,7 @@ class DatabaseScreenMixin: self, right_margin, 85, - "Infrastructure Automated.", + "infrastructure.auto()", fill="#6e6e73", font=("SF Pro Text", 18), anchor="ne", @@ -1008,7 +1008,7 @@ class DatabaseScreenMixin: self, right_margin, 40, - "Prole", + "knoe.dev", fill="#6e6e73", font=("SF Pro Text", 32, "bold"), anchor="ne", @@ -1017,7 +1017,7 @@ class DatabaseScreenMixin: self, right_margin, 85, - "Infrastructure Automated.", + "infrastructure.auto()", fill="#6e6e73", font=("SF Pro Text", 18), anchor="ne", diff --git a/knoe/ui/screens/environment.py b/knoe/ui/screens/environment.py index e6f866d..769f805 100644 --- a/knoe/ui/screens/environment.py +++ b/knoe/ui/screens/environment.py @@ -106,7 +106,7 @@ class EnvironmentScreenMixin: self, right_margin, 40, - "Prole", + "knoe.dev", fill="#6e6e73", font=("SF Pro Text", 32, "bold"), anchor="ne", @@ -115,7 +115,7 @@ class EnvironmentScreenMixin: self, right_margin, 85, - "Personal Infrastructure.", + "infrastructure.auto()", fill="#6e6e73", font=("SF Pro Text", 18), anchor="ne", diff --git a/knoe/ui/screens/gitops.py b/knoe/ui/screens/gitops.py index 17bffc5..27cbb74 100644 --- a/knoe/ui/screens/gitops.py +++ b/knoe/ui/screens/gitops.py @@ -1,4 +1,4 @@ -"""GitOps (Gitea) configuration and deployment screen.""" +"""GitOps (Gitea / GitLab) configuration and deployment screen.""" from __future__ import annotations @@ -6,6 +6,7 @@ import os import subprocess import threading import tkinter as tk +from tkinter import ttk from knoe import prole_conf from knoe import screen as ui @@ -26,7 +27,7 @@ class GitOpsScreenMixin: self, right_margin, 40, - "Prole", + "knoe.dev", fill="#6e6e73", font=("SF Pro Text", 32, "bold"), anchor="ne", @@ -35,7 +36,7 @@ class GitOpsScreenMixin: self, right_margin, 85, - "Infrastructure Automated.", + "infrastructure.auto()", fill="#6e6e73", font=("SF Pro Text", 18), anchor="ne", @@ -52,7 +53,8 @@ class GitOpsScreenMixin: y = 310 for label, value in [ - ("Gitea — self-hosted Git + CI (recommended)", "gitea"), + ("Gitea — self-hosted Git + CI (lightweight)", "gitea"), + ("GitLab — full DevOps platform via GitLab Operator", "gitlab"), ("ArgoCD — continuous delivery / GitOps sync", "argocd"), ("None — skip GitOps tooling", "none"), ]: @@ -82,7 +84,7 @@ class GitOpsScreenMixin: 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" + provider in ("gitea", "gitlab") ) self.prole_cfg_data["Optional Features"]["ARGOCD_ENABLED"] = str( provider == "argocd" @@ -91,7 +93,7 @@ class GitOpsScreenMixin: except Exception: pass # Sync boolean vars used by the individual feature screens. - self.gitops_enabled.set(provider == "gitea") + self.gitops_enabled.set(provider in ("gitea", "gitlab")) self.argocd_enabled.set(provider == "argocd") # ------------------------------------------------------------------ @@ -105,7 +107,7 @@ class GitOpsScreenMixin: self, right_margin, 40, - "Prole", + "knoe.dev", fill="#6e6e73", font=("SF Pro Text", 32, "bold"), anchor="ne", @@ -114,7 +116,7 @@ class GitOpsScreenMixin: self, right_margin, 85, - "Infrastructure Automated.", + "infrastructure.auto()", fill="#6e6e73", font=("SF Pro Text", 18), anchor="ne", @@ -122,15 +124,25 @@ class GitOpsScreenMixin: self._render_title("GitOps", y=150) - description = ( - "Deploys self-hosted GitOps (Gitea) via etc/init_gitea.sh.\n" - "Defaults to the 'gitea' namespace; update below to override." - ) + provider = getattr(self, "gitops_provider", None) + provider_name = (provider.get() if provider else None) or "gitea" + if provider_name == "gitlab": + deploy_description = ( + "Deploys self-hosted GitLab via the GitLab Operator (etc/init_gitlab.sh).\n" + "Removes any pre-existing Gitea/git.prole.org config before installing.\n" + "Uses knoe-db (CloudNativePG) as the PostgreSQL data store." + ) + else: + deploy_description = ( + "Deploys self-hosted GitOps via etc/init_gitea.sh.\n" + "Select your GitOps provider and node placement below." + ) + description = deploy_description self._render_paragraph(description, y=210) enable_cb = tk.Checkbutton( self.bg_canvas, - text="Enable GitOps (Gitea)", + text="Enable GitOps", variable=self.gitops_enabled, command=self._on_gitops_toggle, bg="white", @@ -145,48 +157,43 @@ class GitOpsScreenMixin: self._canvas_items.append(enable_window) self._overlay_widgets.append(enable_cb) - # Namespace input + # GitOps provider dropdown ui.canvas_text( self, 48, 290, - "Kubernetes Namespace", + "GitOps Provider", fill="black", font=("SF Pro Text", 12, "bold"), ) - ns_entry = tk.Entry( + provider_combo = ttk.Combobox( self.bg_canvas, - textvariable=self.gitops_namespace, - bg="white", - fg="black", - insertbackground="black", - highlightbackground="#CCCCCC", - highlightthickness=1, - relief="flat", + textvariable=self.gitops_git_provider, + values=["Gitea", "GitLab", "Forgejo"], + state="readonly", font=("SF Pro Text", 11), + width=20, ) - ns_window = self.bg_canvas.create_window( - 48, 315, window=ns_entry, anchor="nw", width=220, height=30 + provider_window = self.bg_canvas.create_window( + 48, 315, window=provider_combo, anchor="nw", width=220, height=30 ) - self._canvas_items.append(ns_window) - self._overlay_widgets.append(ns_entry) + self._canvas_items.append(provider_window) + self._overlay_widgets.append(provider_combo) node_selector_var = getattr(self, "gitops_node_selector", None) if hasattr(self, "_render_k3s_node_selector") and node_selector_var is not None: - y = self._render_k3s_node_selector( + self._render_k3s_node_selector( y=360, target_var=node_selector_var, title="Node selector:", ) - else: - y = 360 initial_status = "Ready" if self.gitops_enabled.get() else "Disabled" self._gitops_status_var = tk.StringVar(value=initial_status) status_item = ui.canvas_text( self, 48, - y, + 415, f"Status: {initial_status}", fill="black", font=("SF Pro Text", 12), @@ -204,13 +211,13 @@ class GitOpsScreenMixin: # Console output self._gitops_console = self._create_console_output( - y=400, title="Deployment Output", width=880, height=330 + y=455, title="Deployment Output", width=880, height=280 ) # Deploy button self._gitops_deploy_button = tk.Button( self.bg_canvas, - text="Deploy Gitea", + text="Deploy GitOps", command=self.run_gitops_deploy, bg="#F5F5DC", fg="black", @@ -249,20 +256,41 @@ class GitOpsScreenMixin: self.update_footer() def worker(): - namespace = ( - (self.gitops_namespace.get() or "").strip() - or os.environ.get("GITEA_NAMESPACE") - or "gitea" - ) + # gitops_git_provider is the combobox on the config page (Gitea/GitLab/Forgejo). + # gitops_provider is the radio-button choice page (gitea/gitlab/argocd/none). + # Prefer gitops_git_provider since that's what the user sees and sets. + git_provider = ( + getattr(self, "gitops_git_provider", None) and + self.gitops_git_provider.get() + ) or "" + if not git_provider: + git_provider = ( + getattr(self, "gitops_provider", None) and + self.gitops_provider.get() + ) or "Gitea" + is_gitlab = git_provider.lower() == "gitlab" + + if is_gitlab: + namespace = os.environ.get("GITLAB_NAMESPACE") or "gitlab" + else: + namespace = os.environ.get("GITEA_NAMESPACE") or "gitea" + env = os.environ.copy() env["PROLE_HOME"] = str(PROJECT_ROOT) env["PROLE_SERVICE"] = str(PROJECT_ROOT) env["NAMESPACE"] = namespace - env["GITEA_NAMESPACE"] = namespace + if is_gitlab: + env["GITLAB_NAMESPACE"] = namespace + else: + env["GITEA_NAMESPACE"] = namespace - self._gitops_console.write("Starting Gitea deployment...\n") + if is_gitlab: + self._gitops_console.write("Starting GitLab deployment via GitLab Operator...\n") + script_path = PROJECT_ROOT / "etc" / "init_gitlab.sh" + else: + self._gitops_console.write("Starting Gitea deployment...\n") + script_path = PROJECT_ROOT / "etc" / "init_gitea.sh" - script_path = PROJECT_ROOT / "etc" / "init_gitea.sh" if not script_path.exists(): self._gitops_console.write(f"Error: {script_path} not found.\n") self.safe_after( @@ -285,9 +313,14 @@ class GitOpsScreenMixin: mode = "k3d" args = ["--mode", mode, "--namespace", namespace] node_selector = (self.gitops_node_selector.get() or "").strip() - if mode == "k3s" and node_selector: - env["GITEA_NODE_SELECTOR"] = node_selector - env["NODE_SELECTOR"] = node_selector + if node_selector: + if is_gitlab: + env["GITLAB_NODE_SELECTOR"] = node_selector + env["NODE_SELECTOR"] = node_selector + args.extend(["--node-selector", node_selector]) + elif mode == "k3s": + env["GITEA_NODE_SELECTOR"] = node_selector + env["NODE_SELECTOR"] = node_selector self._gitops_console.write(f"Node selector: {node_selector}\n") cfg_path = None @@ -349,16 +382,18 @@ class GitOpsScreenMixin: self.safe_after( lambda: self._gitops_status_var.set("Deployed Successfully") ) + label = "GitLab" if is_gitlab else "GitOps" self._gitops_console.write( - "\nGitOps deployment completed successfully.\n" + f"\n{label} deployment completed successfully.\n" ) else: self._gitops_success = False self.safe_after( lambda: self._gitops_status_var.set(f"Failed (Code {rc})") ) + label = "GitLab" if is_gitlab else "GitOps" self._gitops_console.write( - f"\nGitOps deployment failed with exit code {rc}.\n" + f"\n{label} deployment failed with exit code {rc}.\n" ) self._gitops_deploying = False diff --git a/knoe/ui/screens/navigation.py b/knoe/ui/screens/navigation.py index 26a9a38..98b3b8e 100644 --- a/knoe/ui/screens/navigation.py +++ b/knoe/ui/screens/navigation.py @@ -18,13 +18,13 @@ class NavigationMixin: Notes: - We set Tk's appname for consistency. - - On macOS, attempt to set the process/menu name to 'Prole Database Installer' + - On macOS, attempt to set the process/menu name to 'Knoe System Installer' via NSProcessInfo if PyObjC is available. - Prefer the Prole Tools.app .icns from the built app; fallback to local PNG/GIF. """ # Set Tk application name try: - self.root.tk.call("tk", "appname", "Prole Database Installer") + self.root.tk.call("tk", "appname", "Knoe System Installer") except Exception: pass @@ -34,11 +34,11 @@ class NavigationMixin: try: from Foundation import NSProcessInfo - NSProcessInfo.processInfo().setProcessName_("Prole Database Installer") + NSProcessInfo.processInfo().setProcessName_("Knoe System Installer") except Exception: pass - # Also try to retitle the first main menu item so the menu next to the Apple logo reads 'Prole Database Installer' + # Also try to retitle the first main menu item so the menu next to the Apple logo reads 'Knoe System Installer' try: from AppKit import NSApplication @@ -47,7 +47,7 @@ class NavigationMixin: if main_menu is not None and main_menu.numberOfItems() > 0: first_item = main_menu.itemAtIndex_(0) if first_item is not None: - first_item.setTitle_("Prole Database Installer") + first_item.setTitle_("Knoe System Installer") except Exception: pass @@ -349,11 +349,8 @@ class NavigationMixin: if current_id == "kerberos_config": self.show_page("init_scripts") return - if current_id == "gitops_choice": - self.show_page("kerberos_config") - return if current_id == "argocd_config": - self.show_page("gitops_choice") + self.show_page("kerberos_config") return if current_id == "gitops_config": self.show_page("argocd_config") @@ -619,7 +616,7 @@ class NavigationMixin: if self.kerberos_enabled.get(): self.show_page("kerberos_config") else: - self.show_page("gitops_choice") + self.show_page("argocd_config") return if current_id == "kerberos_config": @@ -661,15 +658,6 @@ 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": @@ -708,11 +696,29 @@ class NavigationMixin: else "Attempted" ) ) + # Derive the correct namespace from the chosen provider. + # gitops_git_provider (combobox: Gitea/GitLab/Forgejo) is the active selector. + git_provider = ( + getattr(self, "gitops_git_provider", None) and + self.gitops_git_provider.get() + ) or "" + if not git_provider: + git_provider = ( + getattr(self, "gitops_provider", None) and + self.gitops_provider.get() + ) or "Gitea" + is_gitlab = git_provider.lower() == "gitlab" + if is_gitlab: + gitops_ns = (self.gitops_namespace.get() or "gitlab").strip() or "gitlab" + else: + gitops_ns = (self.gitops_namespace.get() or "gitea").strip() or "gitea" self.prole_cfg_data["GitOps"] = { "STATUS": gitops_status, - "GITOPS_NAMESPACE": (self.gitops_namespace.get() or "gitea").strip() - or "gitea", + "GITOPS_PROVIDER": git_provider.lower(), + "GITOPS_NAMESPACE": gitops_ns, } + if is_gitlab: + self.prole_cfg_data["GitOps"]["GITLAB_NAMESPACE"] = gitops_ns self._save_prole_cfg() self.show_page("supabase_config") return @@ -822,8 +828,6 @@ 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 == "common_services": if getattr(self, "_common_services_success", False): self.next_button.configure(state="normal") diff --git a/knoe/ui/screens/network.py b/knoe/ui/screens/network.py index 8bbb2fb..edf39a6 100644 --- a/knoe/ui/screens/network.py +++ b/knoe/ui/screens/network.py @@ -47,7 +47,7 @@ class NetworkScreenMixin: self, right_margin, 40, - "Prole", + "knoe.dev", fill="#6e6e73", font=("SF Pro Text", 32, "bold"), anchor="ne", @@ -56,7 +56,7 @@ class NetworkScreenMixin: self, right_margin, 85, - "Infrastructure Automated.", + "infrastructure.auto()", fill="#6e6e73", font=("SF Pro Text", 18), anchor="ne", diff --git a/knoe/ui/screens/ollama.py b/knoe/ui/screens/ollama.py index ff14ee6..8e2bdfe 100644 --- a/knoe/ui/screens/ollama.py +++ b/knoe/ui/screens/ollama.py @@ -29,7 +29,7 @@ class OllamaScreenMixin: self, right_margin, 40, - "Prole", + "knoe.dev", fill="#6e6e73", font=("SF Pro Text", 32, "bold"), anchor="ne", @@ -38,7 +38,7 @@ class OllamaScreenMixin: self, right_margin, 85, - "Infrastructure Automated.", + "infrastructure.auto()", fill="#6e6e73", font=("SF Pro Text", 18), anchor="ne", diff --git a/knoe/ui/screens/packaging.py b/knoe/ui/screens/packaging.py index 44587d7..a38d9fa 100644 --- a/knoe/ui/screens/packaging.py +++ b/knoe/ui/screens/packaging.py @@ -30,7 +30,7 @@ class PackagingScreenMixin: self, right_margin, 40, - "Prole", + "knoe.dev", fill="#6e6e73", font=("SF Pro Text", 32, "bold"), anchor="ne", @@ -307,7 +307,7 @@ class PackagingScreenMixin: self, right_margin, 40, - "Prole", + "knoe.dev", fill="#6e6e73", font=("SF Pro Text", 32, "bold"), anchor="ne", @@ -316,7 +316,7 @@ class PackagingScreenMixin: self, right_margin, 85, - "Infrastructure Automated.", + "infrastructure.auto()", fill="#6e6e73", font=("SF Pro Text", 18), anchor="ne", diff --git a/knoe/ui/screens/security.py b/knoe/ui/screens/security.py index 8ea81a4..5b13162 100644 --- a/knoe/ui/screens/security.py +++ b/knoe/ui/screens/security.py @@ -40,7 +40,7 @@ class SecurityScreenMixin: self, right_margin, 40, - "Prole", + "knoe.dev", fill="#6e6e73", font=("SF Pro Text", 32, "bold"), anchor="ne", @@ -49,7 +49,7 @@ class SecurityScreenMixin: self, right_margin, 85, - "Infrastructure Automated.", + "infrastructure.auto()", fill="#6e6e73", font=("SF Pro Text", 18), anchor="ne", diff --git a/knoe/ui/screens/services.py b/knoe/ui/screens/services.py index 33e7f74..e3a0f8d 100644 --- a/knoe/ui/screens/services.py +++ b/knoe/ui/screens/services.py @@ -40,40 +40,171 @@ from knoe.core.policy import ( class ServicesScreenMixin: """Initialization scripts, CNPG deployment and service-layer overlays.""" + # Traffic light color constants + _TL_YELLOW = "#ff9f0a" + _TL_GREEN = "#34c759" + _TL_RED = "#ff3b30" + + def _set_init_light(self, key: str, state: str) -> None: + """Update a traffic light on the Initialization Scripts page. + + state: 'yellow' | 'green' | 'red' + """ + color = { + "yellow": self._TL_YELLOW, + "green": self._TL_GREEN, + "red": self._TL_RED, + }.get(state, self._TL_YELLOW) + light = getattr(self, "_init_traffic_lights", {}).get(key) + if light is None: + return + try: + self.bg_canvas.itemconfig(light, fill=color) + except Exception: + pass + def _render_init_scripts_page(self): - # Letterhead at top right 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", + self, right_margin, 40, "knoe.dev", + 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, right_margin, 85, "infrastructure.auto()", + fill="#6e6e73", font=("SF Pro Text", 18), anchor="ne", ) - self._render_title("Initialization Scripts", y=150) - self._render_paragraph( - "Running initialization scripts to set up Garage, CloudNative-PG, and backups.", - y=200, + + x_label = 48 + gy = 198 # top of traffic-light section + + # --- Traffic light section --- + # Column x positions + col_common = x_label + col_db = 330 + col_plat = 645 + + # Group headers + for col_x, header in ( + (col_common, "Common Services"), + (col_db, "Database Services"), + (col_plat, "Platform Services"), + ): + ui.canvas_text( + self, col_x, gy, header, + fill="#1d1d1f", font=("SF Pro Text", 11, "bold"), anchor="nw", + ) + + # Common Services — one indicator + lights: dict[str, int] = {} + ly = gy + 24 + indicator = self.bg_canvas.create_oval( + col_common, ly, col_common + 14, ly + 14, + fill=self._TL_YELLOW, outline="#8e8e93", width=1, + ) + self._canvas_items.append(indicator) + ui.canvas_text( + self, col_common + 20, ly + 7, "Services Ready", + fill="black", font=("SF Pro Text", 10), anchor="w", + ) + lights["common_services"] = indicator + + # Database Services — knoe-db-1/2/3 + barman-cloud + db_items = [ + ("knoe_db_1", "knoe-db-1"), + ("knoe_db_2", "knoe-db-2"), + ("knoe_db_3", "knoe-db-3"), + ("barman_cloud", "barman-cloud (backup)"), + ] + for i, (key, label) in enumerate(db_items): + ly = gy + 24 + i * 24 + ind = self.bg_canvas.create_oval( + col_db, ly, col_db + 14, ly + 14, + fill=self._TL_YELLOW, outline="#8e8e93", width=1, + ) + self._canvas_items.append(ind) + ui.canvas_text( + self, col_db + 20, ly + 7, label, + fill="black", font=("SF Pro Text", 10), anchor="w", + ) + lights[key] = ind + + # Platform Services — monitoring + kong/ingress + plat_items = [ + ("monitoring", "Monitoring"), + ("kong", "Kong / Ingress"), + ] + for i, (key, label) in enumerate(plat_items): + ly = gy + 24 + i * 24 + ind = self.bg_canvas.create_oval( + col_plat, ly, col_plat + 14, ly + 14, + fill=self._TL_YELLOW, outline="#8e8e93", width=1, + ) + self._canvas_items.append(ind) + ui.canvas_text( + self, col_plat + 20, ly + 7, label, + fill="black", font=("SF Pro Text", 10), anchor="w", + ) + lights[key] = ind + + self._init_traffic_lights = lights + + # Thin separator below lights + sep_y = gy + 125 + self._canvas_items.append( + self.bg_canvas.create_line( + x_label, sep_y, right_margin, sep_y, + fill="#e0e0e0", width=1, + ) ) - y = 260 - y = self._render_init_scripts_section(48, y, 900, 450, title="Execution Output") + # --- Tabbed output section --- + nb_y = sep_y + 10 + nb_height = 415 - # Use tk.Button + notebook_bg = tk.Frame(self.bg_canvas, bg="white", highlightthickness=0, bd=0) + self.script_tabs = ttk.Notebook(notebook_bg, style="TNotebook") + self.script_tabs.pack(fill="both", expand=True, padx=1, pady=1) + + tab_win = self.bg_canvas.create_window( + x_label, nb_y, window=notebook_bg, anchor="nw", + width=right_margin - x_label, height=nb_height, + ) + self._canvas_items.append(tab_win) + self._overlay_widgets.extend([notebook_bg, self.script_tabs]) + + self.script_consoles = {} + self._script_tab_index = {} + + tab_defs = [ + ("Common Services", "tab_common"), + ("Database Services", "tab_database"), + ("Platform Services", "tab_platform"), + ("Kubectl Status", self.KUBECTL_STATUS_TAB), + ] + for idx, (tab_title, key) in enumerate(tab_defs): + console_bg = tk.Frame(self.script_tabs, bg="white", highlightthickness=0, bd=0) + self.script_tabs.add(console_bg, text=tab_title) + if key == self.KUBECTL_STATUS_TAB: + mono = ("Menlo", 9) if platform.system() == "Darwin" else ("Consolas", 9) + console = ui.TerminalConsole( + console_bg, highlightthickness=0, bd=0, + font=mono, wrap="none", show_horizontal=True, + ) + else: + console = ui.TerminalConsole(console_bg, highlightthickness=0, bd=0) + console.pack(fill="both", expand=True, padx=1, pady=1) + self.script_consoles[key] = console + self._script_tab_index[key] = idx + self._overlay_widgets.extend([console_bg, console]) + + self.script_tabs.bind("<>", self._on_init_scripts_tab_changed) + self.safe_after(self._refresh_kubectl_status_tab, delay=50) + + # Run button + status label + btn_y = nb_y + nb_height + 12 self._init_scripts_button = tk.Button( self.bg_canvas, text="Run Scripts", @@ -89,105 +220,42 @@ class ServicesScreenMixin: pady=8, ) btn_window = self.bg_canvas.create_window( - 48, y + 20, window=self._init_scripts_button, anchor="nw", width=180 + x_label, btn_y, window=self._init_scripts_button, anchor="nw", width=180 ) self._canvas_items.append(btn_window) self._overlay_widgets.append(self._init_scripts_button) - # Status Label self._init_scripts_status_label = ui.canvas_text( - self, 240, y + 32, "", fill="black", font=("SF Pro Text", 12) + self, x_label + 196, btn_y + 12, "", fill="black", font=("SF Pro Text", 12) ) self._canvas_items.append(self._init_scripts_status_label) - def _render_init_scripts_section( - self, - x: int, - y: int, - width: int, - height: int, - title: str | None = "Execution Output", - ) -> int: - if title: - title_item = ui.canvas_text( - self, x, y, title, fill="#1d1d1f", font=("SF Pro Text", 12, "bold") - ) - self._canvas_items.append(title_item) - y += 30 - - # Use a background frame for the notebook to hide potential system borders - notebook_bg = tk.Frame(self.bg_canvas, bg="white", highlightthickness=0, bd=0) - self.script_tabs = ttk.Notebook(notebook_bg, style="TNotebook") - self.script_tabs.pack(fill="both", expand=True, padx=1, pady=1) - - tab_window = self.bg_canvas.create_window( - x, y, window=notebook_bg, anchor="nw", width=width, height=height + # On page load: if previously completed check live state; else stay yellow + already_done = getattr(self, "_scripts_success", False) or ( + (self.prole_cfg_data.get("Initialization Scripts") or {}).get("STATUS") == "Completed" ) - self._canvas_items.append(tab_window) - self._overlay_widgets.append(notebook_bg) - self._overlay_widgets.append(self.script_tabs) - - self.script_consoles = {} - scripts = self._init_scripts_list() - self._script_tab_index = {} - - for idx, (title, fname) in enumerate(scripts): - # Use a background frame to ensure NO borders are visible around the console - console_bg = tk.Frame( - self.script_tabs, bg="white", highlightthickness=0, bd=0 - ) - self.script_tabs.add(console_bg, text=title) - # Use TerminalConsole for consistent styling - if fname == self.KUBECTL_STATUS_TAB: - mono = ( - ("Menlo", 9) if platform.system() == "Darwin" else ("Consolas", 9) - ) - console = ui.TerminalConsole( - console_bg, - highlightthickness=0, - bd=0, - font=mono, - wrap="none", - show_horizontal=True, - ) - else: - console = ui.TerminalConsole(console_bg, highlightthickness=0, bd=0) - console.pack(fill="both", expand=True, padx=1, pady=1) - self.script_consoles[fname] = console - self._script_tab_index[fname] = idx - self._overlay_widgets.append(console_bg) - self._overlay_widgets.append(console) - - self.script_tabs.bind( - "<>", self._on_init_scripts_tab_changed - ) - self.safe_after(self._refresh_kubectl_status_tab, delay=50) - - return y + height - - def _init_scripts_list(self): - scripts = [ - ("Kubectl Status", self.KUBECTL_STATUS_TAB), - ("Common Services", "init_common_services.sh"), - ("Common Services Log", "common_services_log"), - ("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(("Monitoring", "init_monitoring.sh")) - mode = self._deployment_mode() - if mode != "k3d": - scripts.append(("Nginx Ingress", "init_nginx_ingress.sh")) - if mode != "k3s": - scripts.append(("Port Forwards", "init_port_forwards.sh")) - return scripts + if already_done: + self.safe_after(self._check_init_scripts_status_async) def run_init_scripts(self): self._action_flags["init_scripts.run_scripts"] = True def worker(): + # Map script filenames → category tab key + _SCRIPT_TO_TAB = { + "init_common_services.sh": "tab_common", + "common_services_log": "tab_common", + "init_cloudnative_pg.sh": "tab_database", + "init_cnpg_backup.sh": "tab_database", + "init_kong.sh": "tab_platform", + "init_monitoring.sh": "tab_platform", + "init_nginx_ingress.sh": "tab_platform", + "init_port_forwards.sh": "tab_platform", + } + def _select_tab(script_name): - idx = getattr(self, "_script_tab_index", {}).get(script_name) + cat = _SCRIPT_TO_TAB.get(script_name, script_name) + idx = getattr(self, "_script_tab_index", {}).get(cat) if idx is None: return self.safe_after( @@ -198,6 +266,16 @@ class ServicesScreenMixin: ) ) + def _con(script_name): + """Return the console for a script (mapped to category tab).""" + cat = _SCRIPT_TO_TAB.get(script_name, script_name) + return self.script_consoles.get(cat) or self.script_consoles.get(script_name) + + # Reset all traffic lights to yellow at start of run + for _lk in ("common_services", "knoe_db_1", "knoe_db_2", "knoe_db_3", + "barman_cloud", "monitoring", "kong"): + self.safe_after(lambda k=_lk: self._set_init_light(k, "yellow")) + self.safe_after( lambda: ( self._init_scripts_button.configure(state="disabled") @@ -304,18 +382,14 @@ class ServicesScreenMixin: if overall_success: script = "init_common_services.sh" _select_tab(script) - self.script_consoles[script].clear() - self.script_consoles[script].write(f"Running {script} start...\n") + _con(script).clear() + _con(script).write(f"Running {script} start...\n") svc_log_path = _log_path_for(script) self._last_common_services_log_path = svc_log_path env["COMMON_SERVICES_INIT_LOG"] = str(svc_log_path) self._record_install_log(svc_log_path) - self.script_consoles["common_services_log"].clear() - self.script_consoles["common_services_log"].write( - f"Log file: {svc_log_path}\n\n" - ) - self.script_consoles[script].write(f"Log file: {svc_log_path}\n\n") + _con(script).write(f"Log file: {svc_log_path}\n\n") try: svc_fp = svc_log_path.open("a", encoding="utf-8") @@ -323,8 +397,7 @@ class ServicesScreenMixin: svc_fp = None def _svc_line(line): - self.script_consoles[script].write(line) - self.script_consoles["common_services_log"].write(line) + _con("init_common_services.sh").write(line) self._process_script_output_line(line) if svc_fp: try: @@ -388,27 +461,27 @@ class ServicesScreenMixin: pass if rc_svc != 0: - self.script_consoles[script].write( + _con(script).write( f"\nERROR: {script} start failed with code {rc_svc}\n" ) overall_success = False + self.safe_after(lambda: self._set_init_light("common_services", "red")) else: - self.script_consoles[script].write( - f"\n{script} completed successfully.\n" - ) + _con(script).write(f"\n{script} completed successfully.\n") + self.safe_after(lambda: self._set_init_light("common_services", "green")) else: - self.script_consoles["init_common_services.sh"].write( + _con("init_common_services.sh").write( "Skipping common services initialization because previous steps failed.\n" ) # 2. CloudNative-PG initialization (Python) - cnpg_tab = "init_cloudnative_pg.sh" + cnpg_script = "init_cloudnative_pg.sh" if overall_success: - _select_tab(cnpg_tab) - self.script_consoles[cnpg_tab].clear() - self.script_consoles[cnpg_tab].write("Running CloudNative-PG initialization...\n") + _select_tab(cnpg_script) + _con(cnpg_script).clear() + _con(cnpg_script).write("Running CloudNative-PG initialization...\n") - log_path = _log_path_for(cnpg_tab) + log_path = _log_path_for(cnpg_script) self._record_install_log(log_path) try: log_fp = log_path.open("a", encoding="utf-8") @@ -417,7 +490,7 @@ class ServicesScreenMixin: def _cnpg_log(msg): line = msg if msg.endswith("\n") else msg + "\n" - self.script_consoles[cnpg_tab].write(line) + _con("init_cloudnative_pg.sh").write(line) self._process_script_output_line(line) if log_fp: try: @@ -438,14 +511,18 @@ class ServicesScreenMixin: log=_cnpg_log, mode=mode, ) - self.script_consoles[cnpg_tab].write( + _con(cnpg_script).write( "\nCloudNative-PG initialization completed successfully.\n" ) + for _lk in ("knoe_db_1", "knoe_db_2", "knoe_db_3"): + self.safe_after(lambda k=_lk: self._set_init_light(k, "green")) except Exception as _cnpg_exc: - self.script_consoles[cnpg_tab].write( + _con(cnpg_script).write( f"\nERROR: CloudNative-PG initialization failed: {_cnpg_exc}\n" ) overall_success = False + for _lk in ("knoe_db_1", "knoe_db_2", "knoe_db_3"): + self.safe_after(lambda k=_lk: self._set_init_light(k, "red")) finally: if log_fp: try: @@ -453,17 +530,16 @@ class ServicesScreenMixin: except Exception: pass else: - if cnpg_tab in self.script_consoles: - self.script_consoles[cnpg_tab].write( - "Skipping CloudNative-PG initialization because previous steps failed.\n" - ) + _con("init_cloudnative_pg.sh").write( + "Skipping CloudNative-PG initialization because previous steps failed.\n" + ) # 4. init_cnpg_backup.sh start if overall_success: script = "init_cnpg_backup.sh" _select_tab(script) - self.script_consoles[script].clear() - self.script_consoles[script].write(f"Running {script} start...\n") + _con(script).clear() + _con(script).write(f"Running {script} start...\n") log_path = _log_path_for(script) self._record_install_log(log_path) @@ -473,7 +549,7 @@ class ServicesScreenMixin: log_fp = None def _backup_line(line): - self.script_consoles[script].write(line) + _con("init_cnpg_backup.sh").write(line) self._process_script_output_line(line) if log_fp: try: @@ -492,75 +568,65 @@ class ServicesScreenMixin: pass if rc_backup != 0: - self.script_consoles[script].write( - f"\nERROR: {script} start failed with code {rc_backup}\n" - ) + _con(script).write(f"\nERROR: {script} start failed with code {rc_backup}\n") overall_success = False + self.safe_after(lambda: self._set_init_light("barman_cloud", "red")) else: - self.script_consoles[script].write( - f"\n{script} completed successfully.\n" - ) + _con(script).write(f"\n{script} completed successfully.\n") + self.safe_after(lambda: self._set_init_light("barman_cloud", "green")) else: - self.script_consoles["init_cnpg_backup.sh"].write( + _con("init_cnpg_backup.sh").write( "Skipping Prole DB backup because previous steps failed.\n" ) # 5. init_kong.sh start if overall_success: script = "init_kong.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") + _select_tab(script) + _con(script).clear() + _con(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 + 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 _kong_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 - - kong_ns = env.get("NAMESPACE") or "knoe-db" - rc_kong = self.controller.run_script( - script, args=mode_args + ["-n", kong_ns, "start"], env=env, on_line=_kong_line - ) + def _kong_line(line): + _con("init_kong.sh").write(line) + self._process_script_output_line(line) if log_fp: try: - log_fp.close() + log_fp.write(line) + log_fp.flush() except Exception: pass - if rc_kong != 0: - self.script_consoles[script].write( - f"\nERROR: {script} start failed with code {rc_kong}\n" - ) - overall_success = False - else: - self.script_consoles[script].write( - f"\n{script} completed successfully.\n" - ) + kong_ns = env.get("NAMESPACE") or "knoe-db" + rc_kong = self.controller.run_script( + script, args=mode_args + ["-n", kong_ns, "start"], env=env, on_line=_kong_line + ) + if log_fp: + try: + log_fp.close() + except Exception: + pass + + if rc_kong != 0: + _con(script).write(f"\nERROR: {script} start failed with code {rc_kong}\n") + overall_success = False else: - if "init_kong.sh" in self.script_consoles: - self.script_consoles["init_kong.sh"].write( - "Skipping Kong because previous steps failed.\n" - ) + _con("init_kong.sh").write( + "Skipping Kong because previous steps failed.\n" + ) # 6. init_monitoring.sh initialize if overall_success and opt_allowed: script = "init_monitoring.sh" _select_tab(script) - self.script_consoles[script].clear() - self.script_consoles[script].write(f"Running {script} initialize...\n") + _con(script).clear() + _con(script).write(f"Running {script} initialize...\n") log_path = _log_path_for(script) self._record_install_log(log_path) @@ -570,7 +636,7 @@ class ServicesScreenMixin: log_fp = None def _monitoring_line(line): - self.script_consoles[script].write(line) + _con("init_monitoring.sh").write(line) self._process_script_output_line(line) if log_fp: try: @@ -612,52 +678,46 @@ class ServicesScreenMixin: pass if rc_mon != 0: - self.script_consoles[script].write( + _con(script).write( f"\nERROR: {script} initialize failed with code {rc_mon}\n" ) overall_success = False + self.safe_after(lambda: self._set_init_light("monitoring", "red")) else: mon = self.prole_cfg_data.get("Monitoring", {}) or {} if not str(mon.get("GRAFANA_ADMIN_PASSWORD") or "").strip(): - mon["GRAFANA_ADMIN_PASSWORD"] = env.get( - "GRAFANA_ADMIN_PASSWORD", "" - ) + mon["GRAFANA_ADMIN_PASSWORD"] = env.get("GRAFANA_ADMIN_PASSWORD", "") self.prole_cfg_data["Monitoring"] = mon try: self._save_prole_cfg() except Exception: pass - self.script_consoles[script].write( - f"\n{script} completed successfully.\n" - ) + _con(script).write(f"\n{script} completed successfully.\n") + self.safe_after(lambda: self._set_init_light("monitoring", "green")) elif overall_success and not opt_allowed: script = "init_monitoring.sh" _select_tab(script) - self.script_consoles[script].clear() - msg = f"[SKIP] Monitoring disabled by policy: {opt_reason}\n" - self.script_consoles[script].write(msg) - - mon = (getattr(self, "prole_cfg_data", {}) or {}).get("Monitoring") - if mon is None: - mon = {} + _con(script).clear() + _con(script).write(f"[SKIP] Monitoring disabled by policy: {opt_reason}\n") + mon = (getattr(self, "prole_cfg_data", {}) or {}).get("Monitoring") or {} mon["STATUS"] = "Skipped" self.prole_cfg_data["Monitoring"] = mon try: self._save_prole_cfg() except Exception: pass + self.safe_after(lambda: self._set_init_light("monitoring", "green")) else: - if "init_monitoring.sh" in self.script_consoles: - self.script_consoles["init_monitoring.sh"].write( - "Skipping Monitoring initialization because previous steps failed.\n" - ) + _con("init_monitoring.sh").write( + "Skipping Monitoring initialization because previous steps failed.\n" + ) # 7. 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) - self.script_consoles[script].clear() - self.script_consoles[script].write(f"Running {script} initialize...\n") + _con(script).clear() + _con(script).write(f"Running {script} initialize...\n") log_path = _log_path_for(script) self._record_install_log(log_path) try: @@ -666,7 +726,7 @@ class ServicesScreenMixin: log_fp = None def _ingress_line(line): - self.script_consoles[script].write(line) + _con("init_nginx_ingress.sh").write(line) self._process_script_output_line(line) if log_fp: try: @@ -676,10 +736,7 @@ class ServicesScreenMixin: pass rc_ing = self.controller.run_script( - script, - args=mode_args + ["initialize"], - env=env, - on_line=_ingress_line, + script, args=mode_args + ["initialize"], env=env, on_line=_ingress_line, ) if log_fp: try: @@ -687,80 +744,70 @@ class ServicesScreenMixin: except Exception: pass if rc_ing != 0: - self.script_consoles[script].write( - f"\nERROR: {script} initialize failed with code {rc_ing}\n" - ) + _con(script).write(f"\nERROR: {script} initialize failed with code {rc_ing}\n") overall_success = False + self.safe_after(lambda: self._set_init_light("kong", "red")) else: - self.script_consoles[script].write( - f"\n{script} completed successfully.\n" - ) + _con(script).write(f"\n{script} completed successfully.\n") + self.safe_after(lambda: self._set_init_light("kong", "green")) elif mode != "k3d": - self.script_consoles["init_nginx_ingress.sh"].write( + _con("init_nginx_ingress.sh").write( "Skipping Nginx Ingress because previous steps failed.\n" ) # 8. 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: - _select_tab(script) - self.script_consoles[script].clear() - self.script_consoles[script].write(f"Running {script} start...\n") + _select_tab(script) + _con(script).clear() + _con(script).write(f"Running {script} start...\n") - def _pf_line(line): - self.script_consoles[script].write(line) - self._process_script_output_line(line) + def _pf_line(line): + _con("init_port_forwards.sh").write(line) + self._process_script_output_line(line) - env_pf = dict(env) - env_pf.setdefault("PORT_FORWARD_SKIP_VALIDATE", "1") - env_pf.setdefault("PORT_FORWARD_SKIP_WAIT", "1") - env_pf.setdefault("PORT_FORWARD_WAIT_TIMEOUT", "20") - env_pf.setdefault("PORT_FORWARD_WAIT_INTERVAL", "2") + env_pf = dict(env) + env_pf.setdefault("PORT_FORWARD_SKIP_VALIDATE", "1") + env_pf.setdefault("PORT_FORWARD_SKIP_WAIT", "1") + env_pf.setdefault("PORT_FORWARD_WAIT_TIMEOUT", "20") + env_pf.setdefault("PORT_FORWARD_WAIT_INTERVAL", "2") - rc_pf = self.controller.run_script( - script, - args=["--force", "--mode", mode, "start"], - env=env_pf, - on_line=_pf_line, - ) - if rc_pf != 0: - self.script_consoles[script].write( - f"\nERROR: {script} start failed with code {rc_pf}\n" + rc_pf = self.controller.run_script( + script, args=["--force", "--mode", mode, "start"], + env=env_pf, on_line=_pf_line, + ) + if rc_pf != 0: + _con(script).write(f"\nERROR: {script} start failed with code {rc_pf}\n") + overall_success = False + self.safe_after(lambda: self._set_init_light("kong", "red")) + else: + running = False + for _ in range(10): + status_lines = [] + + def _status_line(line): + status_lines.append(line) + _con("init_port_forwards.sh").write(line) + + rc_status = self.controller.run_script( + script, args=["--mode", mode, "status", "grafana"], + env=env_pf, on_line=_status_line, ) + running = any( + l.lstrip().startswith("RUNNING") and "grafana" in l + for l in status_lines + ) + if rc_status == 0 and running: + break + time.sleep(1) + + if not running: + _con(script).write("\nERROR: Grafana port-forward not running after start.\n") overall_success = False + self.safe_after(lambda: self._set_init_light("kong", "red")) else: - running = False - for _ in range(10): - status_lines = [] - - def _status_line(line): - status_lines.append(line) - self.script_consoles[script].write(line) - - rc_status = self.controller.run_script( - script, - args=["--mode", mode, "status", "grafana"], - env=env_pf, - on_line=_status_line, - ) - running = any( - l.lstrip().startswith("RUNNING") and "grafana" in l - for l in status_lines - ) - if rc_status == 0 and running: - break - time.sleep(1) - - if not running: - self.script_consoles[script].write( - "\nERROR: Grafana port-forward not running after start.\n" - ) - overall_success = False - else: - self.script_consoles[script].write( - "\nPort-forwards running (grafana ok).\n" - ) + _con(script).write("\nPort-forwards running (grafana ok).\n") + self.safe_after(lambda: self._set_init_light("kong", "green")) # Verify critical secrets ns = ( @@ -774,7 +821,7 @@ class ServicesScreenMixin: kubectl_base_cmd = resolved_base_cmd except Exception: pass - self.script_consoles["init_cloudnative_pg.sh"].write( + _con("init_cloudnative_pg.sh").write( f"\nVerifying critical secrets in namespace {ns}...\n" ) critical_secrets = ["knoe-db-user", "knoe-db-superuser", "cnpg-admin-key"] @@ -789,10 +836,10 @@ class ServicesScreenMixin: has_secret_check_warnings = False if missing_secrets: has_secret_check_warnings = True - self.script_consoles["init_cloudnative_pg.sh"].write( + _con("init_cloudnative_pg.sh").write( f"WARNING: Missing secrets in namespace '{ns}': {', '.join(missing_secrets)}.\n" ) - self.script_consoles["init_cloudnative_pg.sh"].write( + _con("init_cloudnative_pg.sh").write( "Continuing because initialization scripts succeeded; verify CNPG secrets before production use.\n" ) @@ -991,6 +1038,96 @@ class ServicesScreenMixin: threading.Thread(target=worker, daemon=True).start() + def _check_init_scripts_status_async(self): + """Query the live cluster and flip traffic lights to reflect current state. + + Called on page load when a previous successful run is recorded, so the + user sees green lights without re-running the scripts. + """ + def worker(): + ns = ( + (self.db_namespace.get() or "").strip() + or os.environ.get("NAMESPACE") + or "default" + ) + base_cmd = list(self._kubectl_base_cmd()) + + def _kubectl(*args): + try: + res = subprocess.run( + base_cmd + list(args), + capture_output=True, text=True, + env=os.environ.copy(), + ) + return res.returncode, res.stdout, res.stderr + except Exception as exc: + return 1, "", str(exc) + + # Common Services: any running pod in the service namespace + svc_ns = (self._get_service_namespace() or ns).strip() or ns + rc, out, _ = _kubectl( + "get", "pods", "-n", svc_ns, + "--field-selector=status.phase=Running", "-o", "name", + ) + cs_ok = rc == 0 and bool(out.strip()) + self.safe_after( + lambda ok=cs_ok: self._set_init_light("common_services", "green" if ok else "yellow") + ) + + # Database Services: knoe-db-1/2/3 pods + cnpg_cluster = ( + os.environ.get("CNPG_CLUSTER_NAME") + or os.environ.get("CLUSTER_NAME") + or "knoe-db" + ) + for i, key in enumerate(("knoe_db_1", "knoe_db_2", "knoe_db_3"), 1): + pod_name = f"{cnpg_cluster}-{i}" + rc_p, out_p, _ = _kubectl("get", "pod", pod_name, "-n", ns) + is_ready = rc_p == 0 and "Running" in out_p + self.safe_after( + lambda ok=is_ready, k=key: self._set_init_light(k, "green" if ok else "yellow") + ) + + # Barman-cloud: any completed backup + rc_b, out_b, _ = _kubectl( + "get", "backup", "-n", ns, "-o", + "jsonpath={.items[*].status.phase}", + ) + barman_ok = rc_b == 0 and "completed" in out_b.lower() + self.safe_after( + lambda ok=barman_ok: self._set_init_light("barman_cloud", "green" if ok else "yellow") + ) + + # Monitoring: any running pod in monitoring namespace + mon_ns = os.environ.get("MONITORING_NAMESPACE") or "monitoring" + rc_m, out_m, _ = _kubectl( + "get", "pods", "-n", mon_ns, + "--field-selector=status.phase=Running", "-o", "name", + ) + mon_ok = rc_m == 0 and bool(out_m.strip()) + self.safe_after( + lambda ok=mon_ok: self._set_init_light("monitoring", "green" if ok else "yellow") + ) + + # Kong / Ingress: check kong or ingress-nginx namespace + kong_ok = False + for ns_check in ("kong", "ingress-nginx", ns): + rc_k, out_k, _ = _kubectl( + "get", "pods", "-n", ns_check, + "--field-selector=status.phase=Running", "-o", "name", + ) + if rc_k == 0 and any( + "kong" in line or "ingress" in line + for line in out_k.splitlines() + ): + kong_ok = True + break + self.safe_after( + lambda ok=kong_ok: self._set_init_light("kong", "green" if ok else "yellow") + ) + + threading.Thread(target=worker, daemon=True).start() + def _render_init_cnpg_deploy_page(self): # Letterhead at top right content_width = self.bg_canvas.winfo_width() or 975 @@ -1000,7 +1137,7 @@ class ServicesScreenMixin: self, right_margin, 40, - "Prole", + "knoe.dev", fill="#6e6e73", font=("SF Pro Text", 32, "bold"), anchor="ne", @@ -1009,7 +1146,7 @@ class ServicesScreenMixin: self, right_margin, 85, - "Infrastructure Automated.", + "infrastructure.auto()", fill="#6e6e73", font=("SF Pro Text", 18), anchor="ne", @@ -1412,7 +1549,7 @@ class ServicesScreenMixin: self, right_margin, 40, - "Prole", + "knoe.dev", fill="#6e6e73", font=("SF Pro Text", 32, "bold"), anchor="ne", @@ -1421,7 +1558,7 @@ class ServicesScreenMixin: self, right_margin, 85, - "Infrastructure Automated.", + "infrastructure.auto()", fill="#6e6e73", font=("SF Pro Text", 18), anchor="ne", @@ -1586,7 +1723,7 @@ class ServicesScreenMixin: self, right_margin, 40, - "Prole", + "knoe.dev", fill="#6e6e73", font=("SF Pro Text", 32, "bold"), anchor="ne", @@ -1595,7 +1732,7 @@ class ServicesScreenMixin: self, right_margin, 85, - "Infrastructure Automated.", + "infrastructure.auto()", fill="#6e6e73", font=("SF Pro Text", 18), anchor="ne", diff --git a/knoe/ui/screens/supabase.py b/knoe/ui/screens/supabase.py index 8785fc8..aad15a7 100644 --- a/knoe/ui/screens/supabase.py +++ b/knoe/ui/screens/supabase.py @@ -21,7 +21,7 @@ class SupabaseScreenMixin: self, right_margin, 40, - "Prole", + "knoe.dev", fill="#6e6e73", font=("SF Pro Text", 32, "bold"), anchor="ne", @@ -30,7 +30,7 @@ class SupabaseScreenMixin: self, right_margin, 85, - "Infrastructure Automated.", + "infrastructure.auto()", fill="#6e6e73", font=("SF Pro Text", 18), anchor="ne", @@ -95,30 +95,8 @@ class SupabaseScreenMixin: self._canvas_items.append(node_entry_window) self._overlay_widgets.append(node_entry) y += 36 - storage_label = ui.canvas_text( - self, - 48, - y, - "Storage directory:", - fill="black", - font=("SF Pro Text", 12), - ) - self._canvas_items.append(storage_label) - storage_entry = tk.Entry( - self.bg_canvas, - textvariable=self.supabase_pv_base_dir, - font=("SF Pro Text", 11), - width=38, - relief="solid", - bd=1, - ) - storage_entry_window = self.bg_canvas.create_window( - 190, y - 2, window=storage_entry, anchor="nw" - ) - self._canvas_items.append(storage_entry_window) - self._overlay_widgets.append(storage_entry) - y += 42 + y += 16 initial_status = "Ready" if self.supabase_enabled.get() else "Disabled" self._supabase_status_var = tk.StringVar(value=initial_status) status_item = ui.canvas_text( diff --git a/knoe/ui/screens/welcome.py b/knoe/ui/screens/welcome.py index bf37ad4..73328a4 100644 --- a/knoe/ui/screens/welcome.py +++ b/knoe/ui/screens/welcome.py @@ -19,7 +19,7 @@ class WelcomeScreenMixin: self, right_margin, 40, - "Prole", + "knoe.dev", fill="#6e6e73", font=("SF Pro Text", 32, "bold"), anchor="ne", @@ -28,7 +28,7 @@ class WelcomeScreenMixin: self, right_margin, 85, - "Infrastructure Automated.", + "infrastructure.auto()", fill="#6e6e73", font=("SF Pro Text", 18), anchor="ne", diff --git a/modes/k3s/knoe-db/.version b/modes/k3s/knoe-db/.version index b000479..9da06a1 100644 --- a/modes/k3s/knoe-db/.version +++ b/modes/k3s/knoe-db/.version @@ -1 +1 @@ -159 \ No newline at end of file +160 \ No newline at end of file diff --git a/prole.sh b/prole.sh index 4933f15..60e0bd5 100755 --- a/prole.sh +++ b/prole.sh @@ -44,11 +44,11 @@ usage() { echo echo "Options:" echo " -l, --log Create a datestamped logfile in prole/logs including all stdout/stderr" - echo " -v, --verbose Pass a verbose switch to all functions called by ansible and install.py" - echo " -d, --debug Enable a debug logfile in prole/logs with high verbosity from install.py" + echo " -v, --verbose Pass a verbose switch to all functions called by ansible and install.sh" + echo " -d, --debug Enable a debug logfile in prole/logs with high verbosity from install.sh" echo " -c, --config Path to prole.cfg config file (default: conf/prole.cfg)" - echo " --coverage Create a coverage report for the run of install.py" - echo " -s, --silent Run unattended install (passes -S to install.py)" + echo " --coverage Create a coverage report for the run of install.sh" + echo " -s, --silent Run unattended install (passes -S to install.sh)" echo " -r, --reset Perform a reset before action (site/deploy/install); for canonical full reset run use './prole.sh --reset'" echo echo "Commands:" @@ -108,7 +108,7 @@ if [[ "${VERBOSE}" -eq 1 ]]; then export ANSIBLE_FORCE_COLOR=1 fi -# We separate arguments that are only for install.py +# We separate arguments that are only for install.sh INSTALL_ONLY_ARGS=() if [[ "${DEBUG}" -eq 1 ]]; then INSTALL_ONLY_ARGS+=("--debug") @@ -711,7 +711,7 @@ PY fi if [[ $_next_is_config -eq 1 ]]; then _next_is_config=0 - # Pass -c through to install.py + # Pass -c through to install.sh _INSTALL_PASSTHRU+=("-c" "$_arg") continue fi @@ -753,7 +753,7 @@ PY fi CMD_ARGS=("${_INSTALL_PASSTHRU[@]}") - # Ensure OpenBao URL is set appropriately for the active mode before running install.py. + # Ensure OpenBao URL is set appropriately for the active mode before running install.sh. # Honour top-level -c / --config if provided (CONFIG_PATH), falling back to default. _cfg_for_env="${CONFIG_PATH:-${PROLE_CFG_DEFAULT}}" for ((i=0; i<${#CMD_ARGS[@]}; i++)); do @@ -771,16 +771,14 @@ PY ensure_openbao_url_for_mode "${_mode_for_env}" unset _cfg_for_env _mode_for_env - PY_CMD=("python3") if [[ "${COVERAGE}" -eq 1 ]]; then - PY_CMD=("coverage" "run" "--source=installer") + PY_CMD=("python3" "-m" "coverage" "run" "--source=knoe" "-m" "knoe.ui.screens" "--no-gui") + else + PY_CMD=("${ROOT_DIR}/install.sh" "--no-gui") fi - PY_CMD+=("${ROOT_DIR}/install.py" "--no-gui") - + if [[ "${LOG}" -eq 1 ]]; then run_with_log "${LOGFILE}" "${PY_CMD[@]}" "${INSTALL_ONLY_ARGS[@]}" "${CMD_ARGS[@]}" - elif [[ "${COVERAGE}" -eq 1 ]]; then - "${PY_CMD[@]}" "${INSTALL_ONLY_ARGS[@]}" "${CMD_ARGS[@]}" else "${PY_CMD[@]}" "${INSTALL_ONLY_ARGS[@]}" "${CMD_ARGS[@]}" fi @@ -880,7 +878,7 @@ PY echo "Step 3/3: Running silent installer..." # INSTALL_ONLY_ARGS includes -S and --reset if flags were set - python3 "${ROOT_DIR}/install.py" "--no-gui" "-c" "${ROOT_DIR}/conf/prole.cfg" "${INSTALL_ONLY_ARGS[@]}" "${CMD_ARGS[@]}" + "${ROOT_DIR}/install.sh" "--no-gui" "-c" "${ROOT_DIR}/conf/prole.cfg" "${INSTALL_ONLY_ARGS[@]}" "${CMD_ARGS[@]}" echo "Deployment completed successfully!" ) | tee "${LOGFILE}" @@ -936,11 +934,11 @@ PY install_args+=("--reset") fi - PY_CMD=("python3") if [[ "${COVERAGE}" -eq 1 ]]; then - PY_CMD=("coverage" "run" "--source=installer") + python3 -m coverage run --source=knoe -m knoe.ui.screens --no-gui -S -c "${PROLE_CFG_DEFAULT}" "${install_args[@]}" + else + "${ROOT_DIR}/install.sh" --no-gui -S -c "${PROLE_CFG_DEFAULT}" "${install_args[@]}" fi - "${PY_CMD[@]}" "${ROOT_DIR}/install.py" --no-gui -S -c "${PROLE_CFG_DEFAULT}" "${install_args[@]}" if [[ "${COVERAGE}" -eq 1 ]]; then echo "" diff --git a/prole/tools/run_cnpg_coverage.py b/prole/tools/run_cnpg_coverage.py index fb977e4..75ec222 100644 --- a/prole/tools/run_cnpg_coverage.py +++ b/prole/tools/run_cnpg_coverage.py @@ -73,7 +73,7 @@ def _run_coverage(mode: str, namespace: str, cluster: str, project_root: Path) - f"--source={_CNPG_MODULE}", "--append", f"--data-file={cov_file}", - str(project_root / "install.py"), + "-m", "knoe.ui.screens", "--mode", mode, "--non-interactive", ] diff --git a/run_with_coverage.sh b/run_with_coverage.sh index fe31878..5bbbd7a 100755 --- a/run_with_coverage.sh +++ b/run_with_coverage.sh @@ -9,7 +9,7 @@ PYTHON="$SCRIPT_DIR/bin/python" export PYTHONPATH="$SCRIPT_DIR:${PYTHONPATH:-}" -"$PYTHON" -m coverage run "$SCRIPT_DIR/install.py" "$@" +"$PYTHON" -m coverage run -m knoe.ui.screens "$@" "$PYTHON" -m coverage report -m "$PYTHON" -m coverage html echo "Execution instrumentation report generated in $SCRIPT_DIR/htmlcov/index.html" diff --git a/scan/network_description.txt b/scan/network_description.txt index e5f1f69..1c0e942 100644 --- a/scan/network_description.txt +++ b/scan/network_description.txt @@ -1,59 +1,55 @@ Network Discovery Summary: Primary Router: 10.0.0.1 (eero_5d:50:f2) -DNS Servers: 100.100.100.100, 10.0.0.4, 10.0.0.5 +DNS Servers: 100.100.100.100, 10.0.0.5, 10.0.0.4 Detected Devices: -- 10.0.0.35 [7e:19:9a:fe:9f:ed] (7e:19:9a:fe:9f:ed): Ports [22, 445, 5900, 11434, 88], Services: ['SSH', 'VNC', 'SMB/CIFS (Possible Windows/AD)', 'Ollama', 'Active Directory Related'] -- 10.0.0.33 [ec:b5:fa:b0:76:e4] (PhilipsLight_b0:76:e4): Ports [443], Services: ['Web Server'] -- 10.0.0.170 [b0:ee:7b:ca:73:99] (Roku_ca:73:99): Ports [], Services: [] -- 10.0.0.1 [9c:57:bc:5d:50:f2] (eero_5d:50:f2): Ports [], Services: [] -- 10.0.0.3 [2c:cf:67:8c:2b:47] (RaspberryPi_8c:2b:47): Ports [22, 53, 80, 443, 2049, 5900, 88, 389, 636], Services: ['DNS', 'SSH', 'NFS', 'VNC', 'Web Server', 'Active Directory Related'] -- 10.0.0.5 [b8:27:eb:88:3a:41] (RaspberryPiF_88:3a:41): Ports [22, 2049], Services: ['SSH', 'NFS'] -- 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.203 [00:11:32:3b:2f:08] (Synology_3b:2f:08): Ports [22, 80, 443, 2049, 445], Services: ['SSH', 'NFS', 'Web Server', 'SMB/CIFS (Possible Windows/AD)'] -- 10.0.0.32 [48:a6:b8:a4:8e:cc] (Sonos_a4:8e:cc): Ports [], Services: [] -- 10.0.0.6 [2c:cf:67:45:8d:97] (RaspberryPi_45:8d:97): Ports [22, 443], Services: ['SSH', 'Web Server'] -- 10.0.0.2 [dc:a4:ca:ea:1a:2f] (Apple_ea:1a:2f): Ports [], Services: [] -- 10.0.0.26 [c8:db:26:08:52:f8] (Logitech_08:52:f8): Ports [], Services: [] - 10.0.0.7 [2c:cf:67:ca:93:ce] (RaspberryPi_ca:93:ce): Ports [22], Services: ['SSH'] -- 10.0.0.22 [2c:cf:67:45:8d:97] (RaspberryPi_45:8d:97): Ports [22, 443], Services: ['SSH', '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.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.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.35 [7e:19:9a:fe:9f:ed] (7e:19:9a:fe:9f:ed): Ports [22, 445, 5900, 11434, 88], Services: ['SSH', 'VNC', 'SMB/CIFS (Possible Windows/AD)', 'Ollama', 'Active Directory Related'] +- 10.0.0.26 [c8:db:26:08:52:f8] (Logitech_08:52:f8): Ports [], Services: [] +- 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.32 [48:a6:b8:a4:8e:cc] (Sonos_a4:8e:cc): Ports [], Services: [] +- 10.0.0.22 [2c:cf:67:45:8d:97] (RaspberryPi_45:8d:97): Ports [22, 80, 443], Services: ['SSH', 'Web Server'] +- 10.0.0.2 [dc:a4:ca:ea:1a:2f] (Apple_ea:1a:2f): Ports [], Services: [] +- 10.0.0.1 [9c:57:bc:5d:50:f2] (eero_5d:50:f2): Ports [53], Services: ['DNS'] +- 10.0.0.5 [b8:27:eb:88:3a:41] (RaspberryPiF_88:3a:41): Ports [22, 2049, 5900], Services: ['SSH', 'NFS', 'VNC'] +- 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.33 [ec:b5:fa:b0:76:e4] (PhilipsLight_b0:76:e4): Ports [80, 443], Services: ['Web Server'] - 10.0.0.37 [54:07:7d:22:c0:b2] (Netgear_22:c0:b2): Ports [80], Services: ['Web Server'] -- 10.0.0.46 [d4:f7:d5:40:ab:17] (SonyInteract_40:ab:17): Ports [], Services: [] -- 10.0.0.48 [0c:fe:45:53:f4:3c] (SonyInteract_53:f4:3c): 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.41 [b8:27:eb:57:10:d9] (RaspberryPiF_57:10:d9): Ports [22], Services: ['SSH'] +- 10.0.0.48 [0c:fe:45:53:f4:3c] (SonyInteract_53:f4:3c): Ports [], Services: [] - 10.0.0.52 [2c:cf:67:ca:93:ce] (RaspberryPi_ca:93:ce): Ports [22], Services: ['SSH'] -- 10.0.0.205 [a8:20:66:28:12:e7] (Apple_28:12:e7): Ports [22, 53, 445, 5900, 88], Services: ['DNS', 'SSH', 'VNC', 'SMB/CIFS (Possible Windows/AD)', 'Active Directory Related'] -- 10.0.0.35\ [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.41 [b8:27:eb:57:10:d9] (RaspberryPiF_57:10:d9): Ports [22], Services: ['SSH'] +- 10.0.0.46 [d4:f7:d5:40:ab:17] (SonyInteract_40:ab:17): Ports [], Services: [] - 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: [] -- 10.0.0.58 [7e:46:74:2a:b0:b2] (7e:46:74:2a:b0:b2): Ports [], Services: [] -- 10.0.0.155 [62:17:97:b3:06:a1] (62:17:97:b3:06:a1): Ports [], Services: [] -- 10.0.0.206 [00:a0:de:a2:0b:ef] (Yamaha_a2:0b:ef): Ports [80], Services: ['Web Server'] -- 10.0.0.106 [48:a6:b8:a7:50:60] (Sonos_a7:50:60): Ports [], Services: [] -- 10.0.0.95 [b8:27:eb:dd:6f:14] (RaspberryPiF_dd:6f:14): Ports [22, 2049, 5900], Services: ['SSH', 'NFS', 'VNC'] - 10.0.0.73 [4c:a9:19:b3:12:f8] (TuyaSmart_b3:12:f8): Ports [], Services: [] +- 10.0.0.95 [b8:27:eb:88:3a:41] (RaspberryPiF_88:3a:41): Ports [22, 2049, 5900], Services: ['SSH', 'NFS', 'VNC'] +- 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.205 [a8:20:66:28:12:e7] (Apple_28:12:e7): Ports [22, 53, 445, 5900, 88], Services: ['DNS', 'SSH', 'VNC', 'SMB/CIFS (Possible Windows/AD)', 'Active Directory Related'] +- 10.0.0.188 [00:16:6c:c5:1f:54] (SamsungElect_c5:1f:54): Ports [80, 443], Services: ['Web Server'] - 10.0.0.100 [24:fc:e5:51:cf:74] (SamsungElect_51:cf:74): Ports [], Services: [] -- 10.0.0.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.99 [20:28:bc:f1:dc:ba] (Visionscape_f1:dc:ba): Ports [], Services: [] +- 10.0.0.106 [48:a6:b8:a7:50:60] (Sonos_a7:50:60): Ports [], Services: [] +- 10.0.0.113 [60:81:10:92:02:4e] (Apple_92:02:4e): Ports [], Services: [] - 10.0.0.117 [40:f5:20:1e:5e:91] (Espressif_1e:5e:91): Ports [80], Services: ['Web Server'] -- 10.0.0.107 [b8:06:0d:b7:7c:56] (TuyaSmart_b7:7c:56): Ports [], Services: [] - 10.0.0.123 [3c:ef:8c:96:e3:3c] (ZhejiangDahu_96:e3:3c): Ports [80], Services: ['Web Server'] -- 10.0.0.1\ [9c:57:bc:5d:50:f2] (eero_5d:50:f2): Ports [], Services: [] - 10.0.0.124 [9c:8e:cd:02:73:c4] (AmcrestTechn_02:73:c4): Ports [80], Services: ['Web Server'] - 10.0.0.127 [58:55:ca:4a:60:6e] (Apple_4a:60:6e): Ports [22], Services: ['SSH'] -- 10.0.0.130 [f8:bb:bf:a3:84:eb] (eero_a3:84:eb): Ports [53], Services: ['DNS'] +- 10.0.0.111 [ca:0b:6c:5d:0a:e5] (ca:0b:6c:5d:0a:e5): Ports [], Services: [] +- 10.0.0.107 [b8:06:0d:b7:7c:56] (TuyaSmart_b7:7c:56): Ports [], Services: [] - 10.0.0.125 [a0:60:32:04:73:ba] (AmcrestTechn_04:73:ba): Ports [80], Services: ['Web Server'] -- 10.0.0.143 [60:5f:8d:7a:aa:32] (eero_7a:aa:32): Ports [], Services: [] -- 10.0.0.128 [a8:20:66:28:12:e7] (Apple_28:12:e7): Ports [], Services: [] +- 10.0.0.130 [f8:bb:bf:a3:84:eb] (eero_a3:84:eb): Ports [53], Services: ['DNS'] +- 10.0.0.128 [20:c9:d0:94:30:dd] (Apple_94:30:dd): Ports [22, 53, 445, 5900], Services: ['DNS', 'SSH', 'VNC', 'SMB/CIFS (Possible Windows/AD)'] +- 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.160 [96:c3:fd:58:02:6c] (96:c3:fd:58:02:6c): Ports [], Services: [] -- 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.206 [00:a0:de:a2:0b:ef] (Yamaha_a2:0b:ef): Ports [80], Services: ['Web Server'] +- 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.170 [b0:ee:7b:ca:73:99] (Roku_ca:73:99): 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.196 [60:5f:8d:88:08:f2] (eero_88:08:f2): Ports [53], Services: ['DNS'] - 10.0.0.199 [7c:a6:b0:01:a7:33] (7c:a6:b0:01:a7:33): Ports [], Services: [] +- 10.0.0.1\ [9c:57:bc:5d:50:f2] (eero_5d:50:f2): Ports [], Services: [] Ollama Instances found at: 10.0.0.35, 10.0.0.204 Ansible Inventory Summary: diff --git a/scripts/generate_spec.py b/scripts/generate_spec.py index 81b2dd5..3543c20 100755 --- a/scripts/generate_spec.py +++ b/scripts/generate_spec.py @@ -45,7 +45,7 @@ hiddenimports = [ ] a = Analysis( - ['install.py'], + ['knoe/ui/screens/__main__.py'], pathex=[], binaries=binaries, datas=datas, diff --git a/tests/final_comprehensive_test.sh b/tests/final_comprehensive_test.sh index 32657a2..026fc44 100755 --- a/tests/final_comprehensive_test.sh +++ b/tests/final_comprehensive_test.sh @@ -24,9 +24,9 @@ run_test() { } # Core functionality tests -run_test "Install.py imports" python3 -c "from install import ProleController, get_resource_path" +run_test "Installer imports" python3 -c "from knoe.ui.screens import KnoeController, get_resource_path" run_test "Ncurses modules" python3 -c "from knoe.ncurses_installer import run_ncurses_installer" -run_test "Command-line interface" python3 install.py --help +run_test "Command-line interface" python3 -m knoe.ui.screens --help run_test "Makefile" make help # Resource tests @@ -34,7 +34,7 @@ run_test "Image resources" python3 -c " from pathlib import Path import sys sys.path.insert(0, str(Path.cwd())) -from install import get_resource_path +from knoe.ui.screens import get_resource_path for img in ['img/proleIcon.png', 'img/proleLogo.png', 'img/proleLogoSepia.png']: if not get_resource_path(img).exists(): sys.exit(1) diff --git a/tests/final_test.sh b/tests/final_test.sh index e551f55..a740b61 100755 --- a/tests/final_test.sh +++ b/tests/final_test.sh @@ -25,10 +25,10 @@ test_item() { # Core Features echo "CORE FEATURES" -test_item "Install.py imports" python3 -c "from install import ProleController, get_resource_path" +test_item "Installer imports" python3 -c "from knoe.ui.screens import KnoeController, get_resource_path" test_item "Ncurses interface" python3 -c "from knoe.ncurses_installer import run_ncurses_installer" -test_item "Display auto-detection" python3 -c "from install import has_display" -test_item "CLI arguments" python3 install.py --help +test_item "Display auto-detection" python3 -c "from knoe.ui.screens import has_display" +test_item "CLI arguments" python3 -m knoe.ui.screens --help # Build System echo "" diff --git a/tests/silent_install_test.sh b/tests/silent_install_test.sh index 99b262c..57b9bda 100755 --- a/tests/silent_install_test.sh +++ b/tests/silent_install_test.sh @@ -200,7 +200,7 @@ PY echo "" echo "2. Running silent install..." -if python3 "$ROOT_DIR/install.py" -S -c "$CFG_PATH"; then +if "$ROOT_DIR/install.sh" -S -c "$CFG_PATH"; then echo "OK: Silent install completed" else echo "ERROR: Silent install failed" >&2 diff --git a/tests/test_build_system.sh b/tests/test_build_system.sh index e498979..f00f4ee 100755 --- a/tests/test_build_system.sh +++ b/tests/test_build_system.sh @@ -13,13 +13,13 @@ test -f img/proleIcon.png && echo " ✓ Icon file exists" echo "" echo "3. Testing Python modules..." -python3 -c "from install import ProleController; ProleController('.')" && echo " ✓ ProleController works" +python3 -c "from knoe.ui.screens import KnoeController; KnoeController('.')" && echo " ✓ KnoeController works" python3 -c "from knoe.ncurses_installer import run_ncurses_installer" && echo " ✓ Ncurses installer imports" python3 -c "from knoe.ncurses_ui import CursesWindow" && echo " ✓ Ncurses UI imports" echo "" echo "4. Testing command-line interface..." -python3 install.py --help > /dev/null && echo " ✓ --help works" +python3 -m knoe.ui.screens --help > /dev/null && echo " ✓ --help works" echo "" echo "5. Testing spec generator..." diff --git a/tests/test_install_logic.py b/tests/test_install_logic.py index 7db13a5..f660c4f 100644 --- a/tests/test_install_logic.py +++ b/tests/test_install_logic.py @@ -16,8 +16,8 @@ import os import pytest from pathlib import Path -# Now import install.py after mocking -from install import KnoeInstaller +# Now import after mocking +from knoe.ui.screens import KnoeInstaller @pytest.fixture diff --git a/tests/test_network_scan_fix.sh b/tests/test_network_scan_fix.sh index 03b9480..df4acc9 100755 --- a/tests/test_network_scan_fix.sh +++ b/tests/test_network_scan_fix.sh @@ -65,8 +65,8 @@ cd "$PROLE_ROOT" # Test 5: Code uses scan directory echo "5. Testing code uses scan directory..." -if grep -q "scan_dir = prole_home / \"scan\"" install.py && \ - grep -q "cwd=str(scan_dir)" install.py; then +if grep -rq "scan_dir = prole_home / \"scan\"" knoe/ && \ + grep -rq "cwd=str(scan_dir)" knoe/; then echo " ✓ Code properly uses scan directory" else echo " ✗ Code not updated to use scan directory"