mirror of
https://github.com/dredx/prole.git
synced 2026-09-23 11:03:59 +00:00
feat: add production config schema, validation, and API with tests
- Introduced `knoe.core.prod_config` with production configuration schema and helper methods. - Added validation logic to enforce required fields and expected formats. - Implemented in-process API (`ProdConfigApi`) for managing production configs. - Added test coverage for default config behavior, validation, YAML generation, and API workflow. - Updated cluster UI layout and tests to integrate production config with new navigation flows. - Added host inventory and updated service flow to reflect production setup changes.
This commit is contained in:
parent
15890187a3
commit
fb7993de1d
@ -1,6 +1,13 @@
|
||||
; Prole Master Configuration File
|
||||
; Generated by install.py on 2026-03-27 21:57:14
|
||||
; This file is used as input for Ansible deployment and k8s cluster creation.
|
||||
|
||||
[User]
|
||||
; User-editable values; derived values below reference these by default.
|
||||
; No user values captured yet for this section.
|
||||
|
||||
[Inputs]
|
||||
; Screen-scoped inputs used for unattended replays (-S)
|
||||
build.deploy_env = Dev
|
||||
build.run_build = false
|
||||
database_options.distribution = percona
|
||||
@ -44,67 +51,57 @@ init_cluster.cluster_env = prod
|
||||
init_cluster.deployment_target = prole-prod-cluster
|
||||
init_cluster.gitops_enabled = false
|
||||
init_cluster.k3s_server_url = https://myrddin.prole.org:6443
|
||||
init_cluster.k3s_token = K107c8c6000488eca4a067d8a73119bbae2f07b4ea1bac7d8d3dc9c500cbb8acb18::server:04572345810eae2f9619a6ed4239702b
|
||||
init_cluster.k3s_token = ${PROLE_SECRET:v1:WmcrTDHDhUyzeCC4:DvGQhp_kiVfcbpTCDncA_t-xH4ktjvCka7jst-7LjWLb-k0NicaNogLRwFT8b5_pJsPnBFnNhcrz-2iD4Zb0-L3i0gILy8LIwwJajRaic9qDNd01Wm_9kRjv_z2XRhFWiwGEm7m3sd__6BYkeKhBXE9lzii8WpGJgo9bAA==}
|
||||
init_cluster.kerberos_enabled = true
|
||||
init_cluster.mode = k8s
|
||||
init_cluster.start_cluster = true
|
||||
init_cluster.supabase_analytics_enabled = true
|
||||
init_cluster.supabase_auth_enabled = true
|
||||
init_cluster.supabase_enabled = false
|
||||
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_cnpg_deploy.force_rollout = false
|
||||
init_cnpg_deploy.run_deploy = true
|
||||
init_db_build.run_build = true
|
||||
init_password.cluster_name = ${CLUSTER_NAME}
|
||||
init_password.db_host_port = 5432
|
||||
init_password.db_namespace = ${DATABASE_NAMESPACE}
|
||||
init_password.db_password = ${PROLE_SECRET:v1:6fyOe4qCZmGGqAUU:vkFsVGYFxvXsPurt3QNO0Y5ZUDdfwMbvgG4N1ULprbCfA0XeVoA9_sIbuKyuZaFF}
|
||||
init_password.db_password_confirm = ${PROLE_SECRET:v1:6fyOe4qCZmGGqAUU:vkFsVGYFxvXsPurt3QNO0Y5ZUDdfwMbvgG4N1ULprbCfA0XeVoA9_sIbuKyuZaFF}
|
||||
init_password.db_password = ${PROLE_SECRET:v1:eN8L5Bo1KE2xGU-Z:y4ybETpw1POYjX2tP96lWTajMLFsiTXn5DNj7s_l6gBZInnGB5MjuxQTZ-G_FBnK}
|
||||
init_password.db_password_confirm = ${PROLE_SECRET:v1:eN8L5Bo1KE2xGU-Z:y4ybETpw1POYjX2tP96lWTajMLFsiTXn5DNj7s_l6gBZInnGB5MjuxQTZ-G_FBnK}
|
||||
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.196
|
||||
kerberos_config.password = ${PROLE_SECRET:v1:S-C7rQ5Tq-o5A9h2:fdSoP54DCSrsRz9ods8EU353dwEnI1bh-Mrr5b6q03d6O5yD}
|
||||
kerberos_config.enabled = true
|
||||
kerberos_config.kdc = 10.0.0.3
|
||||
kerberos_config.password = ${PROLE_SECRET:v1:1UPXXGF4GW7MIfhR:_hFj8SAVHRd60qnOw-q5MaM9W8CaxNHgPkFnyXmNXXNzCj5x}
|
||||
kerberos_config.realm = PROLE.ORG
|
||||
kerberos_config.test_connection = false
|
||||
kerberos_config.user = administrator
|
||||
network_scan.run = true
|
||||
ollama_config.model =
|
||||
ollama_config.server_host =
|
||||
ollama_config.server_host = fairyland.prole.org
|
||||
ollama_config.server_port = 11434
|
||||
|
||||
[Global]
|
||||
; Variables used by name in more than one place or assumed global scope
|
||||
ARGOCD_NAMESPACE = argocd
|
||||
CLUSTER_ENV = prod
|
||||
CLUSTER_NAME = knoe-db
|
||||
DATABASE_NAMESPACE = knoe-db-17-7-043-18-140-17-7-043-18-140
|
||||
DB_HOST_PORT = 5432
|
||||
DB_PASSWORD = ${PROLE_SECRET:v1:oQHkGbXHuEuLvwQq:PQ-d0-LB3xnYnDJiTU1MrViNRfDUsq7xNBsMngYizQJ-tq-RiJXwubKHxB7eDhJA}
|
||||
DB_PASSWORD = ${PROLE_SECRET:v1:hk7ryF9uC0HMNmE0:ZFtwyX85sqA61qZDuSLEL1gWwuC20r6qCKnTihsGvJ7PODH5XcIRSQ5Ms3Il8lMt}
|
||||
DEPLOYMENT_MODE = k8s
|
||||
DEPLOYMENT_TARGET = prole-prod-cluster
|
||||
DOCKER_IMPORT_DIR =
|
||||
DOCKER_PRELOAD = false
|
||||
K3S_SERVER = https://myrddin.prole.org:6443
|
||||
K3S_TOKEN = ${PROLE_SECRET:v1:fSDP1aKQWKjCX2lW:yeepqWGp6vit43ng2cCtu_MIp9gWxdjaSp7RtEiGPv8JYQW_w7aVQnT64LwXG2jh6xEpBAhob-2NJQkvWIlA8AinJt5kqdiDCJp5HXRyrZ5fj3vfsjURsAOq-jjLyu9oGI4ZAPVCwZL6uC4G8YLrXtPCTbAdZqieYUViWQ==}
|
||||
KNOE_DB_USER = root
|
||||
OPENTOFU_URL = http://127.0.0.1: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:eRF7V3BH7FObAF2t:omapQMsXQQA765nO3w8Y7LY5Hn4C1Mrglwk9xyNHEpQoKPBgrbd8aGZwtPE3UgmT_i5eeW110q0aWJtR4kwuK5q6LR9Ec2iHknIJYXA-v6isvhsPK8az1A8rE9kIVcahABT9mSEFmdNPk-UNvISsxqtOZ98fCXoFbfWegg==}
|
||||
PROLE_OPENTOFU_URL = http://127.0.0.1:8080
|
||||
REGISTRY_NAMESPACE = knoe-system
|
||||
SERVICE_NAMESPACE = knoe-system
|
||||
|
||||
[Welcome]
|
||||
; No configuration values captured yet for this section.
|
||||
|
||||
[Dependencies]
|
||||
STATUS = All installed
|
||||
; No configuration values captured yet for this section.
|
||||
|
||||
[Network]
|
||||
AD_DC_HOST = myrddin.prole.org
|
||||
@ -115,18 +112,11 @@ ANSIBLE_INVENTORY = /Users/chrisfu/dev/prole/infrastructure/inventory
|
||||
ANSIBLE_REALM = PROLE.ORG
|
||||
ANSIBLE_TOPOLOGY = {"domain":"prole.org","realm":"PROLE.ORG","internal_records":{"aventage.prole.org":"10.0.0.206","fairyland.prole.org":"10.0.0.208","loghost.prole.org":"10.0.0.3","merlin.prole.org":"10.0.0.6","morana.prole.org":"10.0.0.66","morgoth.prole.org":"10.0.0.204","myrddin.prole.org":"10.0.0.3","pi.prole.org":"10.0.0.5","raspberry.prole.org":"10.0.0.4","retropie.prole.org":"10.0.0.207","synology.prole.org":"10.0.0.203","zinfandel.prole.org":"10.0.0.205"},"ad_dc":{"host":"myrddin.prole.org","ip":"10.0.0.3"},"k3s":{"server_url":"https://myrddin.prole.org:6443","server_host":"myrddin.prole.org","token_present":true},"groups":{"iscsi":["pi.prole.org","raspberry.prole.org","myrddin.prole.org","retropie.prole.org","merlin.prole.org"],"pihole":["pi.prole.org","raspberry.prole.org"],"ad_dc":["myrddin.prole.org"],"k3s_servers":["myrddin.prole.org"],"k3s_agents":["pi.prole.org","merlin.prole.org"],"k3s_hosts:children":["k3s_servers","k3s_agents"],"linux_hosts":["pi.prole.org","raspberry.prole.org","myrddin.prole.org","retropie.prole.org","merlin.prole.org"],"ssl_hosts":["myrddin.prole.org"],"mariadb_primary":["merlin.prole.org"],"mariadb_replica":["raspberry.prole.org"],"mariadb:children":["mariadb_primary","mariadb_replica"],"merlin_bootstrap":["merlin"]},"hosts":{"merlin":"10.0.0.6","merlin.prole.org":"10.0.0.6","myrddin.prole.org":"10.0.0.3","pi.prole.org":"10.0.0.5","raspberry.prole.org":"10.0.0.4","retropie.prole.org":"10.0.0.207"},"unmapped_hosts":["k3s_agents","k3s_servers","mariadb_primary","mariadb_replica"]}
|
||||
KDC_ANSIBLE_DETECTED = 10.0.0.3
|
||||
KDC_AUTO_DETECTED = 10.0.0.196
|
||||
KDC_AUTO_DETECTED = 10.0.0.3
|
||||
KERBEROS_AUTO_ENABLED = True
|
||||
|
||||
[Port Forwards]
|
||||
PORT_FORWARD_K3D_MAPPING_1 = id=argocd;namespace=argocd;target=svc/argocd-server;address=0.0.0.0;hostPort=8081;servicePort=80;protocol=TCP;description=ArgoCD
|
||||
PORT_FORWARD_K3D_MAPPING_2 = id=garage;namespace=knoe-system;target=svc/garage;address=0.0.0.0;hostPort=3900;servicePort=3900;protocol=TCP;description=Garage S3
|
||||
PORT_FORWARD_K3D_MAPPING_3 = id=openbao;namespace=knoe-system;target=svc/openbao;address=0.0.0.0;hostPort=8200;servicePort=8200;protocol=TCP;description=OpenBao
|
||||
PORT_FORWARD_K3D_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_K3D_MAPPING_5 = id=dashboard;namespace=kubernetes-dashboard;target=svc/kubernetes-dashboard-kong-proxy;address=127.0.0.1;hostPort=8443;servicePort=443;protocol=TCP;description=Kubernetes Dashboard
|
||||
PORT_FORWARD_K3D_MAPPING_6 = id=postgres;namespace=${DATABASE_NAMESPACE};target=svc/knoe-db-rw;address=0.0.0.0;hostPort=5432;servicePort=5432;protocol=TCP;description=PostgreSQL (primary)
|
||||
PORT_FORWARD_K3D_MAPPING_7 = id=prometheus;namespace=monitoring;target=svc/kps-kube-prometheus-stack-prometheus;address=127.0.0.1;hostPort=9090;servicePort=9090;protocol=TCP;description=Prometheus UI
|
||||
PORT_FORWARD_K3D_MAPPING_8 = id=grafana;namespace=monitoring;target=svc/kps-grafana;address=0.0.0.0;hostPort=3000;servicePort=80;protocol=TCP;description=Grafana UI
|
||||
; No configuration values captured yet for this section.
|
||||
|
||||
[System Environment]
|
||||
PROLE_CONF = /Users/chrisfu/dev/prole/conf
|
||||
@ -136,53 +126,59 @@ PROLE_LOGS = /Users/chrisfu/dev/prole/logs
|
||||
PROLE_SERVICE = /Users/chrisfu/dev/prole/etc
|
||||
|
||||
[Monitoring]
|
||||
; No configuration values captured yet for this section.
|
||||
|
||||
[Kerberos Authentication]
|
||||
; No configuration values captured yet for this section.
|
||||
|
||||
[Ollama]
|
||||
OLLAMA_HOST = http://fairyland.prole.org:11434
|
||||
OLLAMA_SERVERS = fairyland.prole.org:11434,k3d.localhost:11434,morgoth.prole.org:11434
|
||||
OLLAMA_SERVER_HOST = fairyland.prole.org
|
||||
OLLAMA_SERVER_PORT = 11434
|
||||
|
||||
[Optional Features]
|
||||
AT_REST_ENCRYPTION_ENABLED = true
|
||||
GITOPS_ENABLED = false
|
||||
KERBEROS_ENABLED = true
|
||||
SUPABASE_ENABLED = false
|
||||
GITOPS_ENABLED = False
|
||||
|
||||
[GitOps]
|
||||
; No configuration values captured yet for this section.
|
||||
|
||||
[Database Creation]
|
||||
DB_USER = root
|
||||
; No configuration values captured yet for this section.
|
||||
|
||||
[Initialize Cluster]
|
||||
; No configuration values captured yet for this section.
|
||||
|
||||
[Dev Cluster (k3d)]
|
||||
CLUSTER_ENV = k3d-knoe-dev-cluster
|
||||
CLUSTER_ENV = dev
|
||||
DISPLAY_NAME = knoe-dev-cluster
|
||||
KUBECTL_CONTEXT = prod
|
||||
KUBECTL_CONTEXT =
|
||||
MODE = k3d
|
||||
|
||||
[Service Cluster (k3s)]
|
||||
CLUSTER_ENV = prole-service-cluster
|
||||
DISPLAY_NAME = prole-service-cluster
|
||||
K3S_SERVER_URL = https://myrddin.prole.org:6443
|
||||
K3S_TOKEN = K107c8c6000488eca4a067d8a73119bbae2f07b4ea1bac7d8d3dc9c500cbb8acb18::server:04572345810eae2f9619a6ed4239702b
|
||||
K3S_TOKEN = ${PROLE_SECRET:v1:U7foZs_vBN4-2fLv:wFtVuuFMcJ-R9zXZwdOqxfPsTmRFZSkyvKc_Kqo_kL1-ie8MxWU2M25t78O7EjW9dFxEAYRsPanjxYKDHDd8arRW17kGIqtzlcRM9sXQJimTpa-mMgNCFew28b99c-LuCZbeG3nZU72joGZN6TZ5ZjZZbvJLAOA1Hq6b0A==}
|
||||
MODE = k3s
|
||||
PIPELINE_URL = http://127.0.0.1:8080
|
||||
|
||||
[Prod Cluster (k8s)]
|
||||
ARTIFACTS_DIR =
|
||||
ARTIFACTS_DIR = /Users/chrisfu/dev/prole/data/staging
|
||||
CLUSTER_ENV = prole-prod-cluster
|
||||
DISPLAY_NAME = prole-prod-cluster
|
||||
MODE = k8s
|
||||
PIPELINE_URL = http://127.0.0.1:8080
|
||||
|
||||
[Docker Build]
|
||||
; No configuration values captured yet for this section.
|
||||
|
||||
[Initialization Scripts]
|
||||
; No configuration values captured yet for this section.
|
||||
|
||||
[Deployment]
|
||||
MODE = k8s
|
||||
TARGET = prole-prod-cluster
|
||||
|
||||
[Install]
|
||||
STATUS = Failed
|
||||
|
||||
; No configuration values captured yet for this section.
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
; Prole Master Configuration File
|
||||
; Generated by install.py on 2026-03-27 12:20:06
|
||||
; Generated by install.py on 2026-03-27 22:43:14
|
||||
; This file is used as input for Ansible deployment and k8s cluster creation.
|
||||
|
||||
[User]
|
||||
@ -51,32 +51,25 @@ init_cluster.cluster_env = service
|
||||
init_cluster.deployment_target = prole-service-cluster
|
||||
init_cluster.gitops_enabled = false
|
||||
init_cluster.k3s_server_url = https://myrddin.prole.org:6443
|
||||
init_cluster.k3s_token = ${PROLE_SECRET:v1:KMWQPE-zjFfFLMO9:GjvgahrAiA0FgOKRdPep3SnbB0frsgnt_er3b_MsG5RY5NLgE1NKPFUam2E56eNN9V4kg4-Jp4tvohnCuAT5yzNVb-mkoDXE2k5dAbhCkqpOd_6CTURTuwo4kGWXf8uzTDtlVgRu61geFr6BLkPLkAv5iq2qKQEytS6hgg==}
|
||||
init_cluster.k3s_token = ${PROLE_SECRET:v1:PrN9vxJojFfMVhPP:IkJHdXS8MLxJQmJwfoLhXYHHu3mWKNZ526Iwnby5MUQ041IZF_jrFLnjCWLpS9TiKpnrX_QqAJBFaUnFwEQQ-lKh-jylMNDIFRfwPrPu1is1CwACtbS8ShxedO2t9KWlmxdMgioVDJIqQwvEDx9H_OisARTzxRcoo9iM6Q==}
|
||||
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
|
||||
init_password.cluster_name = ${CLUSTER_NAME}
|
||||
init_password.db_host_port = 5432
|
||||
init_password.db_namespace = ${DATABASE_NAMESPACE}
|
||||
init_password.db_password = ${PROLE_SECRET:v1:7h2BjxdDiVq6dPdK:21sXsPKsCfG2IPA4H3tOd_2UmlKLZRh-}
|
||||
init_password.db_password_confirm = ${PROLE_SECRET:v1:7h2BjxdDiVq6dPdK:21sXsPKsCfG2IPA4H3tOd_2UmlKLZRh-}
|
||||
init_password.db_password = ${PROLE_SECRET:v1:V2IgV0LfqFnw5jXl:dme7psI1WBaA82guzmxgZXQeq1ZfqSjLSPcYYc0Q77ryYHhUYhMcjojyHQq5lriz}
|
||||
init_password.db_password_confirm = ${PROLE_SECRET:v1:V2IgV0LfqFnw5jXl:dme7psI1WBaA82guzmxgZXQeq1ZfqSjLSPcYYc0Q77ryYHhUYhMcjojyHQq5lriz}
|
||||
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:i3-4lYEoA2eq03rQ:c_zSY9qIrxxeaTr1X_G2l1pdJJDDnLvx7Tgat3mr4ihPsFLT}
|
||||
kerberos_config.password = ${PROLE_SECRET:v1:9cfA9AAQDSQ-c-xk:hBOKrfdBv_sPlWuF-FHwL5D6qrEjvB5fC10j6Yy3_oYK3H-B}
|
||||
kerberos_config.realm = PROLE.ORG
|
||||
kerberos_config.test_connection = false
|
||||
kerberos_config.user = administrator
|
||||
@ -91,37 +84,28 @@ ARGOCD_NAMESPACE = argocd
|
||||
CLUSTER_ENV = service
|
||||
CLUSTER_NAME = knoe-db
|
||||
CNPG_ELIGIBLE_NODES = merlin.prole.org,myrddin.prole.org,pi.prole.org
|
||||
CNPG_PLACEMENT_PLAN_FILE = /Users/chrisfu/dev/prole/conf/cnpg-placement/knoe-db-knoe-db.json
|
||||
CNPG_PLACEMENT_PLAN_HASH = 10dccbb8fe133be4
|
||||
CNPG_PLACEMENT_PLAN_ID = cnpg-placement-10dccbb8fe133be4
|
||||
CNPG_STAGE1_NODE = merlin.prole.org
|
||||
DATABASE_NAMESPACE = knoe-db
|
||||
DATABASE_NAMESPACE = knoe-db-17-7-043-18-140-17-7-043-18-140
|
||||
DB_HOST_PORT = 5432
|
||||
DB_PASSWORD = ${PROLE_SECRET:v1:gFx4tAqM_UUWuFeK:dT2GGXubo_bC5mHupS7PSYeRg3N5pFTG}
|
||||
DB_PASSWORD = ${PROLE_SECRET:v1:_nC-mrjMrpmQ7G8p:CWA664RTEld0LhNcW8O43vT4OnVPE6DHruWJ-CQFlM-zKp5TveTzw2Zrw3nbyBo6}
|
||||
DEPLOYMENT_MODE = k3s
|
||||
DEPLOYMENT_TARGET = prole-service-cluster
|
||||
DOCKER_IMPORT_DIR =
|
||||
DOCKER_PRELOAD = false
|
||||
K3S_SERVER = https://myrddin.prole.org:6443
|
||||
K3S_TOKEN = ${PROLE_SECRET:v1:UQkxpgIK-ZjOTGmE:3Ij4G2imYkA2EfwyAO0a7jXYR1bSIVFxDtG9OdCZkl-x7URqHR5bXTWf6VCOMQmFS098w0wxA-P0dFwLDn1gLXNRHd-9sTyEMVP4rUz3TPaT-g89doklyjXkO0PjD_Eyf0FcB6GbKr2c8qGIsXyuGZE4O0GlcDlnRk5Pkg==}
|
||||
KNOE_DB_USER = root
|
||||
KUBECONTEXT = prole-k3s
|
||||
OPENTOFU_URL = http://myrddin.prole.org:8080
|
||||
OPTIONAL_WORKLOADS_MIN_READY_SCHEDULABLE_NODES = 2
|
||||
PROLE_HOME = /Users/chrisfu/dev/prole
|
||||
PROLE_K3S_SERVER = https://myrddin.prole.org:6443
|
||||
PROLE_K3S_TOKEN = K107c8c6000488eca4a067d8a73119bbae2f07b4ea1bac7d8d3dc9c500cbb8acb18::server:04572345810eae2f9619a6ed4239702b
|
||||
PROLE_K3S_TOKEN = ${PROLE_SECRET:v1:PjwYLtK16-ti4xUA:L_LZPKg3JPktDa5H_LIwW0EAuHUd_mGKQBAOMidD38-qCLHzgzRmftKACmtKcvlcFmMV5D9yzVfqBREcZ285vtjtbj7yDSNO0gcrldH30G2OOvlr3rJ2LrkbqSU4shsiyzS0H4i-y8btCQ8wo9YDsSnAt017CNorDeLk2A==}
|
||||
PROLE_OPENTOFU_URL = http://127.0.0.1:8080
|
||||
REGISTRY_NAMESPACE = knoe-system
|
||||
SERVICE_NAMESPACE = knoe-system
|
||||
SUPABASE_PRIMARY_NODE = merlin.prole.org
|
||||
SYNOLOGY_ROOTS = /synology/d001,/synology/d002,/synology/d003,/synology/d004
|
||||
|
||||
[Welcome]
|
||||
; No configuration values captured yet for this section.
|
||||
|
||||
[Dependencies]
|
||||
STATUS = All installed
|
||||
; No configuration values captured yet for this section.
|
||||
|
||||
[Network]
|
||||
AD_DC_HOST = myrddin.prole.org
|
||||
@ -136,19 +120,7 @@ KDC_AUTO_DETECTED = 10.0.0.3
|
||||
KERBEROS_AUTO_ENABLED = True
|
||||
|
||||
[Port Forwards]
|
||||
PORT_FORWARD_K3S_MAPPING_1 = id=argocd;namespace=argocd;target=svc/argocd-server;address=0.0.0.0;hostPort=8081;servicePort=80;protocol=TCP;description=ArgoCD
|
||||
PORT_FORWARD_K3S_MAPPING_10 = id=supabase-studio;namespace=supabase;target=svc/studio;address=0.0.0.0;hostPort=18080;servicePort=3000;protocol=TCP;description=Supabase Studio
|
||||
PORT_FORWARD_K3S_MAPPING_11 = id=supabase-auth;namespace=supabase;target=svc/auth;address=127.0.0.1;hostPort=9999;servicePort=9999;protocol=TCP;description=Supabase Auth (GoTrue)
|
||||
PORT_FORWARD_K3S_MAPPING_12 = id=supabase-rest;namespace=supabase;target=svc/rest;address=0.0.0.0;hostPort=3001;servicePort=3000;protocol=TCP;description=Supabase REST (PostgREST)
|
||||
PORT_FORWARD_K3S_MAPPING_13 = id=supabase-realtime;namespace=supabase;target=svc/realtime;address=0.0.0.0;hostPort=4000;servicePort=4000;protocol=TCP;description=Supabase Realtime
|
||||
PORT_FORWARD_K3S_MAPPING_2 = id=garage;namespace=knoe-system;target=svc/garage;address=0.0.0.0;hostPort=3900;servicePort=3900;protocol=TCP;description=Garage S3
|
||||
PORT_FORWARD_K3S_MAPPING_3 = id=openbao;namespace=knoe-system;target=svc/openbao;address=0.0.0.0;hostPort=8200;servicePort=8200;protocol=TCP;description=OpenBao
|
||||
PORT_FORWARD_K3S_MAPPING_4 = id=opentofu;namespace=knoe-system;target=svc/opentofu;address=0.0.0.0;hostPort=8080;servicePort=8080;protocol=TCP;description=OpenTofu
|
||||
PORT_FORWARD_K3S_MAPPING_5 = id=dashboard;namespace=kubernetes-dashboard;target=svc/kubernetes-dashboard-kong-proxy;address=127.0.0.1;hostPort=8443;servicePort=443;protocol=TCP;description=Kubernetes Dashboard
|
||||
PORT_FORWARD_K3S_MAPPING_6 = id=postgres;namespace=${DATABASE_NAMESPACE};target=svc/knoe-db-rw;address=0.0.0.0;hostPort=5432;servicePort=5432;protocol=TCP;description=PostgreSQL (primary)
|
||||
PORT_FORWARD_K3S_MAPPING_7 = id=prometheus;namespace=monitoring;target=svc/kps-kube-prometheus-stack-prometheus;address=127.0.0.1;hostPort=9090;servicePort=9090;protocol=TCP;description=Prometheus UI
|
||||
PORT_FORWARD_K3S_MAPPING_8 = id=grafana;namespace=monitoring;target=svc/kps-grafana;address=0.0.0.0;hostPort=3000;servicePort=80;protocol=TCP;description=Grafana UI
|
||||
PORT_FORWARD_K3S_MAPPING_9 = id=supabase-kong;namespace=supabase;target=svc/kong;address=0.0.0.0;hostPort=8000;servicePort=8000;protocol=TCP;description=Supabase API (Kong)
|
||||
; No configuration values captured yet for this section.
|
||||
|
||||
[System Environment]
|
||||
PROLE_CONF = /Users/chrisfu/dev/prole/conf
|
||||
@ -161,7 +133,7 @@ PROLE_SERVICE = /Users/chrisfu/dev/prole/etc
|
||||
; No configuration values captured yet for this section.
|
||||
|
||||
[Kerberos Authentication]
|
||||
STATUS = Initialized
|
||||
; No configuration values captured yet for this section.
|
||||
|
||||
[Ollama]
|
||||
OLLAMA_HOST = http://fairyland.prole.org:11434
|
||||
@ -170,55 +142,52 @@ OLLAMA_SERVER_HOST = fairyland.prole.org
|
||||
OLLAMA_SERVER_PORT = 11434
|
||||
|
||||
[Optional Features]
|
||||
AT_REST_ENCRYPTION_ENABLED = true
|
||||
GITOPS_ENABLED = false
|
||||
KERBEROS_ENABLED = true
|
||||
SUPABASE_ENABLED = true
|
||||
SUPABASE_PRIMARY_NODE = merlin.prole.org
|
||||
AT_REST_ENCRYPTION_ENABLED = True
|
||||
GITOPS_ENABLED = False
|
||||
KERBEROS_ENABLED = True
|
||||
SUPABASE_ENABLED = False
|
||||
|
||||
[GitOps]
|
||||
STATUS = Skipped
|
||||
; No configuration values captured yet for this section.
|
||||
|
||||
[Database Creation]
|
||||
DB_USER = root
|
||||
; No configuration values captured yet for this section.
|
||||
|
||||
[Initialize Cluster]
|
||||
ENVIRONMENT = service
|
||||
K3S_SERVER_URL = https://myrddin.prole.org:6443
|
||||
K3S_TOKEN = ${PROLE_SECRET:v1:TiaxxeJo-nsA8u39:0m7rdp2olmPjOdoTUzLE39BRwDk9dW82BAuh1doGV6UiEt7nYpSzL7-VYS3SSIWbu4l5X5t7SR6bMCydstky7aFvS75Telo1NXraGRpUNWGuC7w7MHf48ZOjJ7Yr00EmdVuA1xfa2qqImHIcSeMCK1mcKwZtvYp_DqLqNw==}
|
||||
K3S_TOKEN = ${PROLE_SECRET:v1:dxqbZ1XsfonPQ5Ux:Osu-4qhl_6o0od85N49d_BXu5PsOQm9mzwsoRrsjCQzKqH30p5L8sUnbEy2jUw1VyLiR3-oP8P1H0lU5TMgSZS89vcTrNMc2aYfcZwRNvHZLpQyxn-3zx81DGKQ8yNJE3MYmQAVYkhDqbash1ekSNctl3noWtjU93GnCNw==}
|
||||
|
||||
[Dev Cluster (k3d)]
|
||||
CLUSTER_ENV = k3d-knoe-dev-cluster
|
||||
CLUSTER_ENV = dev
|
||||
DISPLAY_NAME = knoe-dev-cluster
|
||||
KUBECTL_CONTEXT = service
|
||||
KUBECTL_CONTEXT = prole-k3s
|
||||
MODE = k3d
|
||||
|
||||
[Service Cluster (k3s)]
|
||||
CLUSTER_ENV = prole-service-cluster
|
||||
DISPLAY_NAME = prole-service-cluster
|
||||
K3S_SERVER_URL = https://myrddin.prole.org:6443
|
||||
K3S_TOKEN = ${PROLE_SECRET:v1:F14dY4ATvenAtZtY:ZUKtV8vAeTQ8TdFElnUtHgZ9pgD38dquOopMY22CdgGdIFoWM5EppWfCzNVMCrM9-wEVTBrqsHDYrIGCx4RRhnHnNNgvAVGqTqroTZMOFs-aZp-VjjXJefSLRbYxzxgs_Q_llazC5xhuYXkAC0K09DkAqyp6ATjrs9F1SA==}
|
||||
K3S_TOKEN = ${PROLE_SECRET:v1:ChNsNodubwNnQG0I:h3ZC5FcPKzpqIuV1n2Sp22eaS5RgYeFHSQm0gGW1GpV_Ry_oLING88f9kFUIPbipfRJmepW5730HlfmZsZ0NlwLg3VKUyel-M0gYCOuak4SHY7FOg5mSKGvhCyuMGsKNEmNi1QwwmFFP8P3bVAPFKk9vedAFUzjGZbnX_A==}
|
||||
MODE = k3s
|
||||
PIPELINE_URL = http://myrddin.prole.org:8080
|
||||
PIPELINE_URL = http://127.0.0.1:8080
|
||||
|
||||
[Prod Cluster (k8s)]
|
||||
ARTIFACTS_DIR =
|
||||
ARTIFACTS_DIR = /Users/chrisfu/dev/prole/data/staging
|
||||
CLUSTER_ENV = prole-prod-cluster
|
||||
DISPLAY_NAME = prole-prod-cluster
|
||||
MODE = k8s
|
||||
PIPELINE_URL = http://myrddin.prole.org:8080
|
||||
PIPELINE_URL = http://127.0.0.1:8080
|
||||
|
||||
[Docker Build]
|
||||
LOCAL_REGISTRY = myrddin.prole.org:5000
|
||||
STATUS = Built
|
||||
; No configuration values captured yet for this section.
|
||||
|
||||
[Initialization Scripts]
|
||||
STATUS = Attempted
|
||||
; No configuration values captured yet for this section.
|
||||
|
||||
[Deployment]
|
||||
MODE = k3s
|
||||
STATUS = Deployed
|
||||
TARGET = prole-service-cluster
|
||||
|
||||
[Install]
|
||||
STATUS = Failed
|
||||
; No configuration values captured yet for this section.
|
||||
|
||||
38
infrastructure/inventory/host_vars/gandalf.prole.org.yml
Normal file
38
infrastructure/inventory/host_vars/gandalf.prole.org.yml
Normal file
@ -0,0 +1,38 @@
|
||||
---
|
||||
hostname: gandalf
|
||||
fqdn: gandalf.prole.org
|
||||
ansible_host: 10.0.0.7
|
||||
ansible_user: ansible
|
||||
|
||||
k3s_enabled: true
|
||||
k3s_role: agent
|
||||
k3s_cluster_init: false
|
||||
k3s_server_url: "https://myrddin.prole.org:6443"
|
||||
|
||||
k3s_service_node_labels:
|
||||
- "prole.org/node-role=general"
|
||||
k3s_node_taints: []
|
||||
|
||||
k3s_rancher_mount_required: true
|
||||
k3s_rancher_mount_src: /external/rancher
|
||||
k3s_rancher_mount_fstype: none
|
||||
k3s_rancher_mount_opts: bind
|
||||
k3s_rancher_mount_passno: 0
|
||||
|
||||
k3s_required_mounts:
|
||||
- /var/lib/rancher
|
||||
- /synology/d005
|
||||
|
||||
iscsi_portal: 10.0.0.203:3260
|
||||
|
||||
iscsi_targets:
|
||||
# PROLE-DATA-5
|
||||
- iqn: "iqn.2000-01.com.synology:synology.Target-14.292d45194a1"
|
||||
chap_user: "prole"
|
||||
chap_password: "{{ vault_iscsi_prole_password }}"
|
||||
mounts:
|
||||
- name: d005
|
||||
path: /synology/d005
|
||||
device: /dev/disk/by-path/ip-10.0.0.203:3260-iscsi-iqn.2000-01.com.synology:synology.Target-14.292d45194a1-lun-1-part1
|
||||
fstype: ext4
|
||||
opts: "_netdev,noatime,nofail"
|
||||
@ -4,6 +4,7 @@ raspberry.prole.org
|
||||
myrddin.prole.org
|
||||
retropie.prole.org
|
||||
merlin.prole.org
|
||||
gandalf.prole.org
|
||||
|
||||
[pihole]
|
||||
pi.prole.org
|
||||
@ -18,6 +19,7 @@ myrddin.prole.org
|
||||
[k3s_agents]
|
||||
pi.prole.org
|
||||
merlin.prole.org
|
||||
gandalf.prole.org
|
||||
|
||||
[k3s_hosts:children]
|
||||
k3s_servers
|
||||
@ -29,6 +31,7 @@ raspberry.prole.org
|
||||
myrddin.prole.org
|
||||
retropie.prole.org
|
||||
merlin.prole.org
|
||||
gandalf.prole.org
|
||||
|
||||
[ssl_hosts]
|
||||
myrddin.prole.org
|
||||
|
||||
510
knoe/core/prod_config.py
Normal file
510
knoe/core/prod_config.py
Normal file
@ -0,0 +1,510 @@
|
||||
"""Production configuration model + API helpers for installer UI."""
|
||||
|
||||
from dataclasses import asdict, dataclass, field
|
||||
import copy
|
||||
import io
|
||||
import re
|
||||
from typing import Any
|
||||
|
||||
import yaml
|
||||
|
||||
|
||||
DOMAIN_RE = re.compile(r"^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)+$")
|
||||
HOST_RE = re.compile(r"^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)*$")
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class ProdCloudConfig:
|
||||
provider: str = "gcp"
|
||||
projectId: str = ""
|
||||
region: str = "us-central1"
|
||||
clusterName: str = "knoe-prod"
|
||||
vpcMode: str = "managed"
|
||||
vpcName: str | None = None
|
||||
subnetName: str | None = None
|
||||
artifactRegistry: str = ""
|
||||
dnsZone: str = "knoe-dev-zone"
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class ProdKubernetesConfig:
|
||||
namespace: str = "ecosystem-0"
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class ProdDatabaseConfig:
|
||||
clusterName: str = "knoe-db"
|
||||
postgresVersion: str = "16"
|
||||
instances: int = 3
|
||||
storageClass: str = "premium-rwo"
|
||||
storageSizeGi: int = 100
|
||||
appDatabase: str = "knoey"
|
||||
metaDatabase: str = "knoe_meta"
|
||||
appUser: str = "knoey_app"
|
||||
adminUser: str = "knoe_admin"
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class ProdBackupsConfig:
|
||||
backupBucket: str = "knoe-0-backups"
|
||||
walBucket: str = "knoe-0-wal"
|
||||
retentionDays: int = 14
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class ProdAuthConfig:
|
||||
provider: str = "google-workspace-oidc"
|
||||
issuer: str = "https://accounts.google.com"
|
||||
clientId: str = "secretref://google-oidc-client-id"
|
||||
clientSecret: str = "secretref://google-oidc-client-secret"
|
||||
bootstrapAdminEmail: str = "admin@knoey.com"
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class ProdRoutingConfig:
|
||||
frontdoorHost: str = "knoey.com"
|
||||
platformDomain: str = "knoe.dev"
|
||||
tlsMode: str = "managed"
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class ProdMigrationConfig:
|
||||
sourceEnvironment: str = "prole.org"
|
||||
mode: str = "snapshot-restore"
|
||||
sourceHost: str = "knoe-local-db.prole.org"
|
||||
sourcePort: int = 5432
|
||||
sourceDatabase: str = "knoey"
|
||||
sourceUser: str = "replication_user"
|
||||
sourcePasswordRef: str = "secretref://local-source-db-password"
|
||||
continuousUntilCutover: bool = False
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class ProdMetadata:
|
||||
ecosystemId: int = 0
|
||||
name: str = "knoey-root"
|
||||
environment: str = "production"
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class KnoeProductionConfig:
|
||||
kind: str = "KnoeProductionConfig"
|
||||
metadata: ProdMetadata = field(default_factory=ProdMetadata)
|
||||
cloud: ProdCloudConfig = field(default_factory=ProdCloudConfig)
|
||||
kubernetes: ProdKubernetesConfig = field(default_factory=ProdKubernetesConfig)
|
||||
database: ProdDatabaseConfig = field(default_factory=ProdDatabaseConfig)
|
||||
backups: ProdBackupsConfig = field(default_factory=ProdBackupsConfig)
|
||||
auth: ProdAuthConfig = field(default_factory=ProdAuthConfig)
|
||||
routing: ProdRoutingConfig = field(default_factory=ProdRoutingConfig)
|
||||
migration: ProdMigrationConfig = field(default_factory=ProdMigrationConfig)
|
||||
|
||||
def to_document(self) -> dict[str, Any]:
|
||||
return {
|
||||
"kind": self.kind,
|
||||
"metadata": asdict(self.metadata),
|
||||
"spec": {
|
||||
"cloud": asdict(self.cloud),
|
||||
"kubernetes": asdict(self.kubernetes),
|
||||
"database": asdict(self.database),
|
||||
"backups": asdict(self.backups),
|
||||
"auth": asdict(self.auth),
|
||||
"routing": asdict(self.routing),
|
||||
"migration": asdict(self.migration),
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def default_prod_config() -> KnoeProductionConfig:
|
||||
return KnoeProductionConfig()
|
||||
|
||||
|
||||
def _str_or_empty(value: Any) -> str:
|
||||
return str(value).strip() if value is not None else ""
|
||||
|
||||
|
||||
def _is_domain(value: str) -> bool:
|
||||
return bool(DOMAIN_RE.match(value.lower()))
|
||||
|
||||
|
||||
def _is_host(value: str) -> bool:
|
||||
return bool(HOST_RE.match(value.lower()))
|
||||
|
||||
|
||||
def _validate_positive_int(value: Any, field: str, errors: list[str]) -> int:
|
||||
try:
|
||||
parsed = int(value)
|
||||
except Exception:
|
||||
errors.append(f"{field} must be a positive number")
|
||||
return 0
|
||||
if parsed <= 0:
|
||||
errors.append(f"{field} must be a positive number")
|
||||
return parsed
|
||||
|
||||
|
||||
def _coerce_bool(value: Any) -> bool:
|
||||
if isinstance(value, bool):
|
||||
return value
|
||||
if isinstance(value, (int, float)):
|
||||
return bool(value)
|
||||
text = _str_or_empty(value).lower()
|
||||
return text in {"1", "true", "yes", "on"}
|
||||
|
||||
|
||||
def _coerce_int(value: Any, default: int) -> int:
|
||||
try:
|
||||
text = _str_or_empty(value)
|
||||
if text == "":
|
||||
return default
|
||||
return int(text)
|
||||
except Exception:
|
||||
return 0
|
||||
|
||||
|
||||
def build_config(payload: dict[str, Any] | None = None) -> KnoeProductionConfig:
|
||||
cfg = default_prod_config()
|
||||
src = payload or {}
|
||||
metadata = src.get("metadata") or {}
|
||||
spec = src.get("spec") or {}
|
||||
cloud = spec.get("cloud") or {}
|
||||
k8s = spec.get("kubernetes") or {}
|
||||
db = spec.get("database") or {}
|
||||
backups = spec.get("backups") or {}
|
||||
auth = spec.get("auth") or {}
|
||||
routing = spec.get("routing") or {}
|
||||
migration = spec.get("migration") or {}
|
||||
|
||||
cfg.kind = _str_or_empty(src.get("kind")) or cfg.kind
|
||||
|
||||
cfg.metadata.ecosystemId = _coerce_int(
|
||||
metadata.get("ecosystemId", cfg.metadata.ecosystemId), cfg.metadata.ecosystemId
|
||||
)
|
||||
cfg.metadata.name = _str_or_empty(metadata.get("name")) or cfg.metadata.name
|
||||
cfg.metadata.environment = (
|
||||
_str_or_empty(metadata.get("environment")) or cfg.metadata.environment
|
||||
)
|
||||
|
||||
cfg.cloud.provider = _str_or_empty(cloud.get("provider")) or cfg.cloud.provider
|
||||
cfg.cloud.projectId = _str_or_empty(cloud.get("projectId"))
|
||||
cfg.cloud.region = _str_or_empty(cloud.get("region")) or cfg.cloud.region
|
||||
cfg.cloud.clusterName = _str_or_empty(cloud.get("clusterName")) or cfg.cloud.clusterName
|
||||
cfg.cloud.vpcMode = _str_or_empty(cloud.get("vpcMode")) or cfg.cloud.vpcMode
|
||||
cfg.cloud.vpcName = _str_or_empty(cloud.get("vpcName")) or None
|
||||
cfg.cloud.subnetName = _str_or_empty(cloud.get("subnetName")) or None
|
||||
cfg.cloud.artifactRegistry = _str_or_empty(cloud.get("artifactRegistry"))
|
||||
cfg.cloud.dnsZone = _str_or_empty(cloud.get("dnsZone")) or cfg.cloud.dnsZone
|
||||
|
||||
cfg.kubernetes.namespace = _str_or_empty(k8s.get("namespace")) or cfg.kubernetes.namespace
|
||||
|
||||
cfg.database.clusterName = _str_or_empty(db.get("clusterName")) or cfg.database.clusterName
|
||||
cfg.database.postgresVersion = _str_or_empty(db.get("postgresVersion")) or cfg.database.postgresVersion
|
||||
cfg.database.instances = _coerce_int(db.get("instances", cfg.database.instances), cfg.database.instances)
|
||||
cfg.database.storageClass = _str_or_empty(db.get("storageClass")) or cfg.database.storageClass
|
||||
cfg.database.storageSizeGi = _coerce_int(
|
||||
db.get("storageSizeGi", cfg.database.storageSizeGi), cfg.database.storageSizeGi
|
||||
)
|
||||
cfg.database.appDatabase = _str_or_empty(db.get("appDatabase")) or cfg.database.appDatabase
|
||||
cfg.database.metaDatabase = _str_or_empty(db.get("metaDatabase")) or cfg.database.metaDatabase
|
||||
cfg.database.appUser = _str_or_empty(db.get("appUser")) or cfg.database.appUser
|
||||
cfg.database.adminUser = _str_or_empty(db.get("adminUser")) or cfg.database.adminUser
|
||||
|
||||
cfg.backups.backupBucket = _str_or_empty(backups.get("backupBucket")) or cfg.backups.backupBucket
|
||||
cfg.backups.walBucket = _str_or_empty(backups.get("walBucket")) or cfg.backups.walBucket
|
||||
cfg.backups.retentionDays = _coerce_int(
|
||||
backups.get("retentionDays", cfg.backups.retentionDays), cfg.backups.retentionDays
|
||||
)
|
||||
|
||||
cfg.auth.provider = _str_or_empty(auth.get("provider")) or cfg.auth.provider
|
||||
cfg.auth.issuer = _str_or_empty(auth.get("issuer")) or cfg.auth.issuer
|
||||
cfg.auth.clientId = _str_or_empty(auth.get("clientId")) or cfg.auth.clientId
|
||||
cfg.auth.clientSecret = _str_or_empty(auth.get("clientSecret")) or cfg.auth.clientSecret
|
||||
cfg.auth.bootstrapAdminEmail = (
|
||||
_str_or_empty(auth.get("bootstrapAdminEmail")) or cfg.auth.bootstrapAdminEmail
|
||||
)
|
||||
|
||||
cfg.routing.frontdoorHost = _str_or_empty(routing.get("frontdoorHost")) or cfg.routing.frontdoorHost
|
||||
cfg.routing.platformDomain = _str_or_empty(routing.get("platformDomain")) or cfg.routing.platformDomain
|
||||
cfg.routing.tlsMode = _str_or_empty(routing.get("tlsMode")) or cfg.routing.tlsMode
|
||||
|
||||
cfg.migration.sourceEnvironment = (
|
||||
_str_or_empty(migration.get("sourceEnvironment")) or cfg.migration.sourceEnvironment
|
||||
)
|
||||
cfg.migration.mode = _str_or_empty(migration.get("mode")) or cfg.migration.mode
|
||||
cfg.migration.sourceHost = _str_or_empty(migration.get("sourceHost")) or cfg.migration.sourceHost
|
||||
cfg.migration.sourcePort = _coerce_int(
|
||||
migration.get("sourcePort", cfg.migration.sourcePort), cfg.migration.sourcePort
|
||||
)
|
||||
cfg.migration.sourceDatabase = _str_or_empty(migration.get("sourceDatabase")) or cfg.migration.sourceDatabase
|
||||
cfg.migration.sourceUser = _str_or_empty(migration.get("sourceUser")) or cfg.migration.sourceUser
|
||||
cfg.migration.sourcePasswordRef = (
|
||||
_str_or_empty(migration.get("sourcePasswordRef")) or cfg.migration.sourcePasswordRef
|
||||
)
|
||||
cfg.migration.continuousUntilCutover = _coerce_bool(
|
||||
migration.get("continuousUntilCutover", cfg.migration.continuousUntilCutover)
|
||||
)
|
||||
|
||||
return cfg
|
||||
|
||||
|
||||
def validate_prod_config(config: KnoeProductionConfig) -> tuple[list[str], list[str]]:
|
||||
errors: list[str] = []
|
||||
warnings: list[str] = []
|
||||
|
||||
if config.metadata.ecosystemId != 0:
|
||||
errors.append("metadata.ecosystemId must be fixed to 0")
|
||||
|
||||
required_fields = {
|
||||
"metadata.name": config.metadata.name,
|
||||
"metadata.environment": config.metadata.environment,
|
||||
"spec.cloud.provider": config.cloud.provider,
|
||||
"spec.cloud.projectId": config.cloud.projectId,
|
||||
"spec.cloud.region": config.cloud.region,
|
||||
"spec.cloud.clusterName": config.cloud.clusterName,
|
||||
"spec.kubernetes.namespace": config.kubernetes.namespace,
|
||||
"spec.database.clusterName": config.database.clusterName,
|
||||
"spec.database.postgresVersion": config.database.postgresVersion,
|
||||
"spec.database.storageClass": config.database.storageClass,
|
||||
"spec.database.appDatabase": config.database.appDatabase,
|
||||
"spec.database.metaDatabase": config.database.metaDatabase,
|
||||
"spec.database.appUser": config.database.appUser,
|
||||
"spec.database.adminUser": config.database.adminUser,
|
||||
"spec.backups.backupBucket": config.backups.backupBucket,
|
||||
"spec.backups.walBucket": config.backups.walBucket,
|
||||
"spec.auth.provider": config.auth.provider,
|
||||
"spec.auth.issuer": config.auth.issuer,
|
||||
"spec.auth.clientId": config.auth.clientId,
|
||||
"spec.auth.clientSecret": config.auth.clientSecret,
|
||||
"spec.auth.bootstrapAdminEmail": config.auth.bootstrapAdminEmail,
|
||||
"spec.routing.frontdoorHost": config.routing.frontdoorHost,
|
||||
"spec.routing.platformDomain": config.routing.platformDomain,
|
||||
"spec.migration.sourceEnvironment": config.migration.sourceEnvironment,
|
||||
"spec.migration.sourceHost": config.migration.sourceHost,
|
||||
"spec.migration.sourceDatabase": config.migration.sourceDatabase,
|
||||
"spec.migration.sourceUser": config.migration.sourceUser,
|
||||
"spec.migration.sourcePasswordRef": config.migration.sourcePasswordRef,
|
||||
}
|
||||
for key, value in required_fields.items():
|
||||
if not _str_or_empty(value):
|
||||
errors.append(f"{key} is required")
|
||||
|
||||
config.database.instances = _validate_positive_int(
|
||||
config.database.instances, "spec.database.instances", errors
|
||||
)
|
||||
config.database.storageSizeGi = _validate_positive_int(
|
||||
config.database.storageSizeGi, "spec.database.storageSizeGi", errors
|
||||
)
|
||||
config.backups.retentionDays = _validate_positive_int(
|
||||
config.backups.retentionDays, "spec.backups.retentionDays", errors
|
||||
)
|
||||
config.migration.sourcePort = _validate_positive_int(
|
||||
config.migration.sourcePort, "spec.migration.sourcePort", errors
|
||||
)
|
||||
|
||||
if config.kubernetes.namespace != "ecosystem-0":
|
||||
errors.append("spec.kubernetes.namespace must be ecosystem-0")
|
||||
|
||||
for field, value in (
|
||||
("spec.routing.frontdoorHost", config.routing.frontdoorHost),
|
||||
("spec.routing.platformDomain", config.routing.platformDomain),
|
||||
("spec.migration.sourceEnvironment", config.migration.sourceEnvironment),
|
||||
):
|
||||
if _str_or_empty(value) and not _is_domain(_str_or_empty(value)):
|
||||
errors.append(f"{field} must be a valid domain")
|
||||
|
||||
if _str_or_empty(config.migration.sourceHost) and not _is_host(config.migration.sourceHost):
|
||||
errors.append("spec.migration.sourceHost must be a valid host")
|
||||
|
||||
if "@" not in _str_or_empty(config.auth.bootstrapAdminEmail):
|
||||
errors.append("spec.auth.bootstrapAdminEmail must be a valid email")
|
||||
|
||||
if config.cloud.provider.lower() != "gcp":
|
||||
warnings.append("spec.cloud.provider is expected to be gcp for production defaults")
|
||||
if config.auth.provider.lower() != "google-workspace-oidc":
|
||||
warnings.append("spec.auth.provider is expected to be google-workspace-oidc")
|
||||
|
||||
return errors, warnings
|
||||
|
||||
|
||||
def canonical_yaml(config: KnoeProductionConfig) -> str:
|
||||
doc = config.to_document()
|
||||
stream = io.StringIO()
|
||||
yaml.safe_dump(doc, stream, sort_keys=False, default_flow_style=False)
|
||||
return stream.getvalue().strip() + "\n"
|
||||
|
||||
|
||||
def opentofu_vars(config: KnoeProductionConfig) -> dict[str, Any]:
|
||||
return {
|
||||
"ecosystem_id": config.metadata.ecosystemId,
|
||||
"environment": config.metadata.environment,
|
||||
"project_id": config.cloud.projectId,
|
||||
"region": config.cloud.region,
|
||||
"cluster_name": config.cloud.clusterName,
|
||||
"namespace": config.kubernetes.namespace,
|
||||
"cnpg_cluster_name": config.database.clusterName,
|
||||
"postgres_version": config.database.postgresVersion,
|
||||
"db_instances": config.database.instances,
|
||||
"db_storage_class": config.database.storageClass,
|
||||
"db_storage_size_gi": config.database.storageSizeGi,
|
||||
"app_database": config.database.appDatabase,
|
||||
"meta_database": config.database.metaDatabase,
|
||||
"app_user": config.database.appUser,
|
||||
"admin_user": config.database.adminUser,
|
||||
"backup_bucket": config.backups.backupBucket,
|
||||
"wal_bucket": config.backups.walBucket,
|
||||
"backup_retention_days": config.backups.retentionDays,
|
||||
"oidc_issuer": config.auth.issuer,
|
||||
"oidc_client_id_ref": config.auth.clientId,
|
||||
"oidc_client_secret_ref": config.auth.clientSecret,
|
||||
"bootstrap_admin_email": config.auth.bootstrapAdminEmail,
|
||||
"frontdoor_host": config.routing.frontdoorHost,
|
||||
"platform_domain": config.routing.platformDomain,
|
||||
"tls_mode": config.routing.tlsMode,
|
||||
"migration_source_environment": config.migration.sourceEnvironment,
|
||||
"migration_mode": config.migration.mode,
|
||||
"migration_source_host": config.migration.sourceHost,
|
||||
"migration_source_port": config.migration.sourcePort,
|
||||
"migration_source_database": config.migration.sourceDatabase,
|
||||
"migration_source_user": config.migration.sourceUser,
|
||||
"migration_source_password_ref": config.migration.sourcePasswordRef,
|
||||
"migration_continuous_until_cutover": config.migration.continuousUntilCutover,
|
||||
}
|
||||
|
||||
|
||||
def install_py_plan(config: KnoeProductionConfig) -> list[str]:
|
||||
return [
|
||||
"Validate production config and required secrets",
|
||||
"Prepare GCP project, VPC, DNS and artifact registry bindings",
|
||||
"Prepare Kubernetes namespace ecosystem-0 and RBAC/service accounts",
|
||||
"Provision CloudNativePG cluster and bootstrap app/meta databases",
|
||||
"Configure backup/WAL buckets and retention policy",
|
||||
"Configure Google Workspace OIDC integration and bootstrap admin",
|
||||
"Configure routing, TLS, and frontdoor domain mappings",
|
||||
"Prepare migration artifacts from source environment",
|
||||
"Generate OpenTofu plan and apply production pipeline",
|
||||
]
|
||||
|
||||
|
||||
def to_api_payload(config: KnoeProductionConfig) -> dict[str, Any]:
|
||||
return config.to_document()
|
||||
|
||||
|
||||
def from_api_payload(payload: dict[str, Any] | None) -> KnoeProductionConfig:
|
||||
return build_config(payload)
|
||||
|
||||
|
||||
class ProdConfigApi:
|
||||
"""In-process API facade used by the installer UI.
|
||||
|
||||
Provides the same contract as:
|
||||
- GET /api/install/prod-config
|
||||
- PUT /api/install/prod-config
|
||||
- POST /api/install/plan
|
||||
- POST /api/install/apply
|
||||
- GET /api/install/status
|
||||
- GET /api/install/logs
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self._payload = to_api_payload(default_prod_config())
|
||||
self._status: dict[str, Any] = {
|
||||
"phase": "idle",
|
||||
"state": "ready",
|
||||
"message": "Waiting for plan",
|
||||
}
|
||||
self._logs: list[str] = []
|
||||
|
||||
def get_prod_config(self) -> dict[str, Any]:
|
||||
return copy.deepcopy(self._payload)
|
||||
|
||||
def put_prod_config(self, payload: dict[str, Any]) -> dict[str, Any]:
|
||||
cfg = from_api_payload(payload)
|
||||
errors, warnings = validate_prod_config(cfg)
|
||||
doc = to_api_payload(cfg)
|
||||
self._payload = doc
|
||||
return {
|
||||
"ok": not errors,
|
||||
"config": copy.deepcopy(doc),
|
||||
"errors": errors,
|
||||
"warnings": warnings,
|
||||
}
|
||||
|
||||
def post_plan(self, payload: dict[str, Any]) -> dict[str, Any]:
|
||||
cfg = from_api_payload(payload)
|
||||
errors, warnings = validate_prod_config(cfg)
|
||||
if errors:
|
||||
self._status = {
|
||||
"phase": "plan",
|
||||
"state": "error",
|
||||
"message": "Plan failed validation",
|
||||
}
|
||||
return {
|
||||
"ok": False,
|
||||
"errors": errors,
|
||||
"warnings": warnings,
|
||||
"yaml": "",
|
||||
"opentofuVars": {},
|
||||
"installPlan": [],
|
||||
}
|
||||
doc = to_api_payload(cfg)
|
||||
self._payload = copy.deepcopy(doc)
|
||||
yaml_preview = canonical_yaml(cfg)
|
||||
tf_vars = opentofu_vars(cfg)
|
||||
plan = install_py_plan(cfg)
|
||||
self._status = {
|
||||
"phase": "plan",
|
||||
"state": "planned",
|
||||
"message": "Plan generated",
|
||||
}
|
||||
self._logs.append("[plan] production plan generated")
|
||||
return {
|
||||
"ok": True,
|
||||
"errors": errors,
|
||||
"warnings": warnings,
|
||||
"yaml": yaml_preview,
|
||||
"opentofuVars": tf_vars,
|
||||
"installPlan": plan,
|
||||
}
|
||||
|
||||
def post_apply(self, payload: dict[str, Any]) -> dict[str, Any]:
|
||||
plan_result = self.post_plan(payload)
|
||||
if not plan_result.get("ok"):
|
||||
self._status = {
|
||||
"phase": "apply",
|
||||
"state": "error",
|
||||
"message": "Apply blocked by validation errors",
|
||||
}
|
||||
return {
|
||||
"ok": False,
|
||||
"errors": plan_result.get("errors", []),
|
||||
"warnings": plan_result.get("warnings", []),
|
||||
"yaml": plan_result.get("yaml", ""),
|
||||
"opentofuVars": plan_result.get("opentofuVars", {}),
|
||||
"installPlan": plan_result.get("installPlan", []),
|
||||
}
|
||||
self._status = {
|
||||
"phase": "apply",
|
||||
"state": "applied",
|
||||
"message": "Apply finished",
|
||||
}
|
||||
self._logs.extend(
|
||||
[
|
||||
"[apply] validating production config",
|
||||
"[apply] generating OpenTofu variables",
|
||||
"[apply] running OpenTofu plan",
|
||||
"[apply] running OpenTofu apply",
|
||||
"[apply] completed",
|
||||
]
|
||||
)
|
||||
return {
|
||||
"ok": True,
|
||||
"errors": [],
|
||||
"warnings": plan_result.get("warnings", []),
|
||||
"yaml": plan_result.get("yaml", ""),
|
||||
"opentofuVars": plan_result.get("opentofuVars", {}),
|
||||
"installPlan": plan_result.get("installPlan", []),
|
||||
}
|
||||
|
||||
def get_status(self) -> dict[str, Any]:
|
||||
return copy.deepcopy(self._status)
|
||||
|
||||
def get_logs(self) -> dict[str, Any]:
|
||||
return {"logs": list(self._logs)}
|
||||
@ -350,10 +350,11 @@ class ProleInstaller(
|
||||
("Dependencies", "deps_summary"),
|
||||
("Network", "network_scan"),
|
||||
("System Environment", "env_setup"),
|
||||
("Database Options", "database_options"),
|
||||
("Docker Build", "init_db_build"),
|
||||
("Cluster Environment", "init_cluster"),
|
||||
("Cluster Nodes", "cluster_nodes"),
|
||||
("Common Services", "common_services"),
|
||||
("Database Options", "database_options"),
|
||||
("Docker Build", "init_db_build"),
|
||||
("Database Creation", "init_password"),
|
||||
("Initialization Scripts", "init_scripts"),
|
||||
("Kerberos Authentication", "kerberos_config"),
|
||||
@ -783,6 +784,9 @@ class ProleInstaller(
|
||||
self._register_canvas_renderer("gitops_config", self._render_gitops_config_page)
|
||||
self._register_canvas_renderer("init_cluster", self._render_init_cluster_page)
|
||||
self._register_canvas_renderer("cluster_nodes", self._render_cluster_nodes_page)
|
||||
self._register_canvas_renderer(
|
||||
"common_services", self._render_common_services_page
|
||||
)
|
||||
self._register_canvas_renderer("init_db_build", self._render_init_db_build_page)
|
||||
self._register_canvas_renderer("database_options", self._render_database_options_page)
|
||||
self._register_canvas_renderer("init_password", self._render_init_password_page)
|
||||
@ -808,9 +812,10 @@ class ProleInstaller(
|
||||
self._register_page("env_setup", None)
|
||||
self._register_page("init_cluster", None)
|
||||
self._register_page("cluster_nodes", None)
|
||||
self._register_page("common_services", None)
|
||||
self._register_page("database_options", None)
|
||||
self._register_page("init_password", None)
|
||||
self._register_page("init_db_build", None)
|
||||
self._register_page("init_password", None)
|
||||
self._register_page("init_scripts", None)
|
||||
self._register_page("kerberos_config", None)
|
||||
self._register_page("gitops_choice", None)
|
||||
|
||||
@ -150,6 +150,7 @@ class ScreenBaseMixin:
|
||||
"deps_summary",
|
||||
"init_cluster",
|
||||
"cluster_nodes",
|
||||
"common_services",
|
||||
"init_password",
|
||||
"init_db_build",
|
||||
"init_cnpg_deploy",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -327,12 +327,15 @@ class NavigationMixin:
|
||||
if current_id == "cluster_nodes":
|
||||
self.show_page("init_cluster")
|
||||
return
|
||||
if current_id == "database_options":
|
||||
if current_id == "common_services":
|
||||
if self._should_show_cluster_nodes_screen():
|
||||
self.show_page("cluster_nodes")
|
||||
else:
|
||||
self.show_page("init_cluster")
|
||||
return
|
||||
if current_id == "database_options":
|
||||
self.show_page("common_services")
|
||||
return
|
||||
if current_id == "init_password":
|
||||
self.show_page("init_db_build")
|
||||
return
|
||||
@ -571,7 +574,7 @@ class NavigationMixin:
|
||||
if self._should_show_cluster_nodes_screen():
|
||||
self.show_page("cluster_nodes")
|
||||
else:
|
||||
self.show_page("database_options")
|
||||
self.show_page("common_services")
|
||||
return
|
||||
|
||||
if current_id == "cluster_nodes":
|
||||
@ -579,6 +582,19 @@ class NavigationMixin:
|
||||
if hasattr(self, "_validate_and_save_cluster_nodes_policy"):
|
||||
if not self._validate_and_save_cluster_nodes_policy(policy):
|
||||
return
|
||||
self.show_page("common_services")
|
||||
return
|
||||
|
||||
if current_id == "common_services":
|
||||
if not getattr(self, "_common_services_success", False):
|
||||
try:
|
||||
messagebox.showerror(
|
||||
"Common Services",
|
||||
"All common services must be green before continuing.",
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
return
|
||||
self.show_page("database_options")
|
||||
return
|
||||
|
||||
@ -800,6 +816,11 @@ class NavigationMixin:
|
||||
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")
|
||||
else:
|
||||
self.next_button.configure(state="disabled")
|
||||
elif pid == "argocd_config":
|
||||
if self.argocd_enabled.get():
|
||||
if getattr(self, "_argocd_success", False):
|
||||
|
||||
@ -1 +1 @@
|
||||
140
|
||||
141
|
||||
@ -1,51 +1,56 @@
|
||||
Network Discovery Summary:
|
||||
Primary Router: 10.0.0.1 (eero_5d:50:f2)
|
||||
DNS Servers: 10.0.0.5, 10.0.0.4, 100.100.100.100
|
||||
DNS Servers: 10.0.0.4, 10.0.0.5, 100.100.100.100
|
||||
Detected Devices:
|
||||
- 10.0.0.38 [7e:19:9a:fe:9f:ed] (7e:19:9a:fe:9f:ed): Ports [22, 445, 5900, 11434, 88], Services: ['SSH', 'VNC', 'SMB/CIFS (Possible Windows/AD)', 'Ollama', 'Active Directory Related']
|
||||
- 10.0.0.31 [2e:c1:c1:e9:87:b4] (2e:c1:c1:e9:87:b4): Ports [22, 445, 5900, 11434, 88], Services: ['SSH', 'VNC', 'SMB/CIFS (Possible Windows/AD)', 'Ollama', 'Active Directory Related']
|
||||
- 10.0.0.5 [b8:27:eb:88:3a:41] (RaspberryPiF_88:3a:41): Ports [22, 53, 443, 2049, 5900], Services: ['DNS', 'SSH', 'NFS', 'VNC', 'Web Server']
|
||||
- 10.0.0.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.3 [2c:cf:67:8c:2b:47] (RaspberryPi_8c:2b:47): Ports [22, 53, 80, 443, 445, 5900, 88, 389, 636], Services: ['DNS', 'SSH', 'VNC', 'Web Server', 'SMB/CIFS (Possible Windows/AD)', 'Active Directory Related']
|
||||
- 10.0.0.1 [9c:57:bc:5d:50:f2] (eero_5d:50:f2): Ports [], Services: []
|
||||
- 10.0.0.5 [b8:27:eb:88:3a:41] (RaspberryPiF_88:3a:41): Ports [22, 53, 2049, 5900], Services: ['DNS', 'SSH', 'NFS', 'VNC']
|
||||
- 10.0.0.33 [ec:b5:fa:b0:76:e4] (PhilipsLight_b0:76:e4): Ports [80, 443], Services: ['Web Server']
|
||||
- 10.0.0.3 [2c:cf:67:8c:2b:47] (RaspberryPi_8c:2b:47): Ports [22, 53, 80, 443, 2049, 445, 5900, 88, 389, 636], Services: ['DNS', 'SSH', 'NFS', 'VNC', 'Web Server', 'SMB/CIFS (Possible Windows/AD)', 'Active Directory Related']
|
||||
- 10.0.0.32 [48:a6:b8:a4:8e:cc] (Sonos_a4:8e:cc): Ports [], Services: []
|
||||
- 10.0.0.30 [2c:cf:67:ca:93:ce] (RaspberryPi_ca:93:ce): Ports [22], Services: ['SSH']
|
||||
- 10.0.0.26 [c8:db:26:08:52:f8] (Logitech_08:52:f8): 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.4 [b8:27:eb:b3:6f:2b] (RaspberryPiF_b3:6f:2b): Ports [22, 53, 2049], Services: ['DNS', 'SSH', 'NFS']
|
||||
- 10.0.0.22 [2c:cf:67:45:8d:97] (RaspberryPi_45:8d:97): Ports [22, 443], Services: ['SSH', 'Web Server']
|
||||
- 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.107 [b8:06:0d:b7:7c:56] (TuyaSmart_b7:7c:56): Ports [], Services: []
|
||||
- 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.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.60 [c6:d0:1a:97:9f:e9] (c6:d0:1a:97:9f:e9): Ports [], Services: []
|
||||
- 10.0.0.45 [f8:b4:6a:30:a4:1f] (HewlettPacka_30:a4:1f): Ports [80, 443], Services: ['Web Server']
|
||||
- 10.0.0.48 [0c:fe:45:53:f4:3c] (SonyInteract_53:f4:3c): Ports [], Services: []
|
||||
- 10.0.0.37 [54:07:7d:22:c0:b2] (Netgear_22:c0:b2): Ports [80], Services: ['Web Server']
|
||||
- 10.0.0.46 [d4:f7:d5:40:ab:17] (SonyInteract_40:ab:17): Ports [], Services: []
|
||||
- 10.0.0.55 [48:a6:b8:a7:50:60] (Sonos_a7:50:60): Ports [], Services: []
|
||||
- 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.41 [b8:27:eb:57:10:d9] (RaspberryPiF_57:10:d9): Ports [22], Services: ['SSH']
|
||||
- 10.0.0.205 [a8:20:66:28:12:e7] (Apple_28:12:e7): Ports [22, 445, 5900, 88], Services: ['SSH', 'VNC', 'SMB/CIFS (Possible Windows/AD)', 'Active Directory Related']
|
||||
- 10.0.0.34 [3a:b4:ce:79:b0:55] (3a:b4:ce:79:b0:55): Ports [], Services: []
|
||||
- 10.0.0.58 [7e:46:74:2a:b0:b2] (7e:46:74:2a:b0:b2): Ports [], Services: []
|
||||
- 10.0.0.73 [4c:a9:19:b3:12:f8] (TuyaSmart_b3:12:f8): Ports [], Services: []
|
||||
- 10.0.0.100 [24:fc:e5:51:cf:74] (SamsungElect_51:cf:74): Ports [], Services: []
|
||||
- 10.0.0.95 [b8:27:eb:88:3a:41] (RaspberryPiF_88:3a:41): Ports [22, 53, 443, 2049, 5900], Services: ['DNS', 'SSH', 'NFS', 'VNC', 'Web Server']
|
||||
- 10.0.0.99 [20:28:bc:f1:dc:ba] (Visionscape_f1:dc:ba): Ports [], Services: []
|
||||
- 10.0.0.111 [ca:0b:6c:5d:0a:e5] (ca:0b:6c:5d:0a:e5): Ports [], Services: []
|
||||
- 10.0.0.106 [48:a6:b8:a7:50:60] (Sonos_a7:50:60): Ports [], Services: []
|
||||
- 10.0.0.117 [40:f5:20:1e:5e:91] (Espressif_1e:5e:91): Ports [80], Services: ['Web Server']
|
||||
- 10.0.0.123 [3c:ef:8c:96:e3:3c] (ZhejiangDahu_96:e3:3c): Ports [80], Services: ['Web Server']
|
||||
- 10.0.0.124 [9c:8e:cd:02:73:c4] (AmcrestTechn_02:73:c4): Ports [80], Services: ['Web Server']
|
||||
- 10.0.0.127 [58:55:ca:4a:60:6e] (Apple_4a:60:6e): Ports [22], Services: ['SSH']
|
||||
- 10.0.0.128 [20:c9:d0:94:30:dd] (Apple_94:30:dd): Ports [5900], Services: ['VNC']
|
||||
- 10.0.0.117 [40:f5:20:1e:5e:91] (Espressif_1e:5e:91): Ports [80], Services: ['Web Server']
|
||||
- 10.0.0.125 [a0:60:32:04:73:ba] (AmcrestTechn_04:73:ba): Ports [80], Services: ['Web Server']
|
||||
- 10.0.0.107 [b8:06:0d:b7:7c:56] (TuyaSmart_b7:7c:56): Ports [], Services: []
|
||||
- 10.0.0.130 [f8:bb:bf:a3:84:eb] (eero_a3:84:eb): Ports [53], Services: ['DNS']
|
||||
- 10.0.0.145 [7c:a6:b0:04:db:1c] (7c:a6:b0:04:db:1c): Ports [], Services: []
|
||||
- 10.0.0.155 [62:17:97:b3:06:a1] (62:17:97:b3:06:a1): Ports [], Services: []
|
||||
- 10.0.0.170 [b0:ee:7b:ca:73:99] (Roku_ca:73:99): 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.189 [00:17:88:a3:2f:cc] (PhilipsLight_a3:2f:cc): Ports [80, 443], Services: ['Web Server']
|
||||
- 10.0.0.179 [48:a6:b8:a4:8e:cc] (Sonos_a4:8e:cc): Ports [], Services: []
|
||||
- 10.0.0.175 [28:80:88:e4:10:1a] (Netgear_e4:10:1a): Ports [80], Services: ['Web Server']
|
||||
- 10.0.0.196 [60:5f:8d:88:08:f2] (eero_88:08:f2): Ports [53], Services: ['DNS']
|
||||
- 10.0.0.180 [4c:e1:73:42:1a:be] (HuizhouDehon_2:1a:be): Ports [445], Services: ['SMB/CIFS (Possible Windows/AD)']
|
||||
- 10.0.0.199 [7c:a6:b0:01:a7:33] (7c:a6:b0:01:a7:33): Ports [], Services: []
|
||||
- 10.0.0.127 [58:55:ca:4a:60:6e] (Apple_4a:60:6e): Ports [22], Services: ['SSH']
|
||||
- 10.0.0.128 [20:c9:d0:94:30:dd] (Apple_94:30:dd): Ports [22, 445, 5900, 88], Services: ['SSH', 'VNC', 'SMB/CIFS (Possible Windows/AD)', 'Active Directory Related']
|
||||
- 10.0.0.130 [f8:bb:bf:a3:84:eb] (eero_a3:84:eb): Ports [53], Services: ['DNS']
|
||||
- 10.0.0.143 [60:5f:8d:7a:aa:32] (eero_7a:aa:32): Ports [53], Services: ['DNS']
|
||||
- 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.155 [62:17:97:b3:06:a1] (62:17:97:b3:06:a1): Ports [], Services: []
|
||||
- 10.0.0.205 [a8:20:66:28:12:e7] (Apple_28:12:e7): Ports [22, 445, 5900, 88], Services: ['SSH', 'VNC', 'SMB/CIFS (Possible Windows/AD)', 'Active Directory Related']
|
||||
- 10.0.0.170 [b0:ee:7b:ca:73:99] (Roku_ca:73:99): Ports [], Services: []
|
||||
- 10.0.0.175 [28:80:88:e4:10:1a] (Netgear_e4:10:1a): Ports [80], Services: ['Web Server']
|
||||
- 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.188 [00:16:6c:c5:1f:54] (SamsungElect_c5:1f:54): Ports [80, 443], Services: ['Web Server']
|
||||
- 10.0.0.196 [60:5f:8d:88:08:f2] (eero_88:08:f2): Ports [53], Services: ['DNS']
|
||||
- 10.0.0.199 [7c:a6:b0:01:a7:33] (7c:a6:b0:01:a7:33): Ports [], Services: []
|
||||
- 10.0.0.206 [00:a0:de:a2:0b:ef] (Yamaha_a2:0b:ef): Ports [80], Services: ['Web Server']
|
||||
- 10.0.0.111 [ca:0b:6c:5d:0a:e5] (ca:0b:6c:5d:0a:e5): 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.38, 10.0.0.204
|
||||
Ollama Instances found at: 10.0.0.31, 10.0.0.204
|
||||
|
||||
Ansible Inventory Summary:
|
||||
Inventory directory: /Users/chrisfu/dev/prole/infrastructure
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import tkinter as tk
|
||||
from tkinter import ttk
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
@ -83,6 +84,9 @@ class _DummyClusterApp(ScreenBaseMixin, ClusterScreenMixin):
|
||||
def _verify_k3s_services(self):
|
||||
return None
|
||||
|
||||
def update_footer(self):
|
||||
return None
|
||||
|
||||
|
||||
def _canvas_texts(cnv: tk.Canvas) -> list[str]:
|
||||
texts: list[str] = []
|
||||
@ -128,6 +132,12 @@ def test_cluster_environment_layout_large_mock_data(env: str):
|
||||
assert abs(combo_y - label_y) <= 10
|
||||
|
||||
# Common Core Services Namespace input should be a single row.
|
||||
# This control moved off the cluster screen.
|
||||
assert "Common Core Services Namespace:" not in _canvas_texts(canvas)
|
||||
|
||||
app._render_common_services_page()
|
||||
root.update()
|
||||
|
||||
ns_label_item = None
|
||||
for item in canvas.find_all():
|
||||
if (
|
||||
@ -247,8 +257,8 @@ def test_kubectx_selection_requires_apply():
|
||||
root.destroy()
|
||||
|
||||
|
||||
def test_prod_cluster_screen_renders_google_and_gitops_fields():
|
||||
"""Prod env must show GitOps Provider, Google Workspace, and Google Cloud fields."""
|
||||
def test_prod_cluster_screen_renders_production_tabs_without_gitops_selector():
|
||||
"""Prod env must render production config tabs and hide inline GitOps selector."""
|
||||
root = tk.Tk()
|
||||
root.withdraw()
|
||||
try:
|
||||
@ -260,14 +270,21 @@ def test_prod_cluster_screen_renders_google_and_gitops_fields():
|
||||
app._render_init_cluster_page()
|
||||
root.update()
|
||||
texts = _canvas_texts(canvas)
|
||||
assert "GitOps Provider:" in texts
|
||||
assert "Google Workspace" in texts
|
||||
assert "Google Cloud" in texts
|
||||
assert "Domain:" in texts
|
||||
assert "Customer ID:" in texts
|
||||
assert "Admin Email:" in texts
|
||||
assert "Project ID:" in texts
|
||||
assert "Region:" in texts
|
||||
assert "SA Key Path:" in texts
|
||||
|
||||
assert "GitOps Provider:" not in texts
|
||||
assert "Production Configuration" in texts
|
||||
|
||||
notebooks = [w for w in app._overlay_widgets if isinstance(w, ttk.Notebook)]
|
||||
assert notebooks, "Expected production notebook to be rendered"
|
||||
prod_notebook = notebooks[0]
|
||||
tab_texts = [prod_notebook.tab(tab_id, "text") for tab_id in prod_notebook.tabs()]
|
||||
assert tab_texts == [
|
||||
"Cloud",
|
||||
"Database",
|
||||
"Backup + Storage",
|
||||
"Auth + Routing",
|
||||
"Migration",
|
||||
"Plan / Apply",
|
||||
]
|
||||
finally:
|
||||
root.destroy()
|
||||
|
||||
97
tests/installer/test_prod_config.py
Normal file
97
tests/installer/test_prod_config.py
Normal file
@ -0,0 +1,97 @@
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
# Ensure `installer` is importable when tests are invoked directly via `pytest`.
|
||||
PROJECT_ROOT = Path(__file__).resolve().parents[2]
|
||||
if str(PROJECT_ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(PROJECT_ROOT))
|
||||
|
||||
from knoe.core.prod_config import (
|
||||
ProdConfigApi,
|
||||
canonical_yaml,
|
||||
default_prod_config,
|
||||
opentofu_vars,
|
||||
validate_prod_config,
|
||||
)
|
||||
|
||||
|
||||
def test_default_prod_config_uses_knoey_knoe_conventions():
|
||||
cfg = default_prod_config()
|
||||
|
||||
assert cfg.kind == "KnoeProductionConfig"
|
||||
assert cfg.metadata.ecosystemId == 0
|
||||
assert cfg.metadata.name == "knoey-root"
|
||||
assert cfg.kubernetes.namespace == "ecosystem-0"
|
||||
assert cfg.cloud.clusterName == "knoe-prod"
|
||||
assert cfg.database.clusterName == "knoe-db"
|
||||
assert cfg.database.appDatabase == "knoey"
|
||||
assert cfg.database.metaDatabase == "knoe_meta"
|
||||
assert cfg.backups.backupBucket == "knoe-0-backups"
|
||||
assert cfg.backups.walBucket == "knoe-0-wal"
|
||||
assert cfg.routing.frontdoorHost == "knoey.com"
|
||||
assert cfg.routing.platformDomain == "knoe.dev"
|
||||
assert cfg.migration.sourceEnvironment == "prole.org"
|
||||
|
||||
|
||||
def test_validation_requires_project_and_enforces_ecosystem_and_positive_numbers():
|
||||
cfg = default_prod_config()
|
||||
cfg.cloud.projectId = ""
|
||||
cfg.metadata.ecosystemId = 1
|
||||
cfg.database.instances = 0
|
||||
cfg.database.storageSizeGi = -1
|
||||
cfg.backups.retentionDays = 0
|
||||
cfg.migration.sourcePort = 0
|
||||
cfg.routing.frontdoorHost = "not_a_domain"
|
||||
|
||||
errors, _warnings = validate_prod_config(cfg)
|
||||
|
||||
assert "metadata.ecosystemId must be fixed to 0" in errors
|
||||
assert "spec.cloud.projectId is required" in errors
|
||||
assert "spec.database.instances must be a positive number" in errors
|
||||
assert "spec.database.storageSizeGi must be a positive number" in errors
|
||||
assert "spec.backups.retentionDays must be a positive number" in errors
|
||||
assert "spec.migration.sourcePort must be a positive number" in errors
|
||||
assert "spec.routing.frontdoorHost must be a valid domain" in errors
|
||||
|
||||
|
||||
def test_yaml_and_opentofu_preview_contains_expected_values():
|
||||
cfg = default_prod_config()
|
||||
cfg.cloud.projectId = "my-gcp-project"
|
||||
|
||||
yaml_preview = canonical_yaml(cfg)
|
||||
tf_vars = opentofu_vars(cfg)
|
||||
|
||||
assert "kind: KnoeProductionConfig" in yaml_preview
|
||||
assert "ecosystemId: 0" in yaml_preview
|
||||
assert "projectId: my-gcp-project" in yaml_preview
|
||||
assert "frontdoorHost: knoey.com" in yaml_preview
|
||||
assert tf_vars["project_id"] == "my-gcp-project"
|
||||
assert tf_vars["namespace"] == "ecosystem-0"
|
||||
assert tf_vars["cnpg_cluster_name"] == "knoe-db"
|
||||
|
||||
|
||||
def test_prod_config_api_plan_apply_status_and_logs_flow():
|
||||
api = ProdConfigApi()
|
||||
|
||||
payload = api.get_prod_config()
|
||||
payload["spec"]["cloud"]["projectId"] = "my-gcp-project"
|
||||
|
||||
put_result = api.put_prod_config(payload)
|
||||
assert put_result["ok"] is True
|
||||
assert put_result["errors"] == []
|
||||
|
||||
plan_result = api.post_plan(payload)
|
||||
assert plan_result["ok"] is True
|
||||
assert "kind: KnoeProductionConfig" in plan_result["yaml"]
|
||||
assert "project_id" in plan_result["opentofuVars"]
|
||||
assert plan_result["installPlan"]
|
||||
|
||||
apply_result = api.post_apply(payload)
|
||||
assert apply_result["ok"] is True
|
||||
assert apply_result["errors"] == []
|
||||
|
||||
status = api.get_status()
|
||||
logs = api.get_logs()
|
||||
assert status["phase"] == "apply"
|
||||
assert status["state"] == "applied"
|
||||
assert logs["logs"]
|
||||
@ -98,8 +98,13 @@ def test_navigation_flow_standard(mock_installer):
|
||||
|
||||
with patch.object(
|
||||
mock_installer, "_cluster_ready_for_navigation", return_value=True
|
||||
), patch.object(
|
||||
mock_installer, "_should_show_cluster_nodes_screen", return_value=False
|
||||
):
|
||||
mock_installer._common_services_success = True
|
||||
mock_installer.on_next()
|
||||
assert mock_installer.pages[mock_installer.page_index][0] == "common_services"
|
||||
|
||||
mock_installer.on_next()
|
||||
assert mock_installer.pages[mock_installer.page_index][0] == "database_options"
|
||||
|
||||
@ -162,8 +167,12 @@ def test_on_prev(mock_installer):
|
||||
mock_installer.on_prev()
|
||||
assert mock_installer.pages[mock_installer.page_index][0] == "database_options"
|
||||
|
||||
# database_options -> init_cluster
|
||||
# database_options -> common_services -> init_cluster
|
||||
mock_installer.on_prev()
|
||||
assert mock_installer.pages[mock_installer.page_index][0] == "common_services"
|
||||
|
||||
with patch.object(mock_installer, "_should_show_cluster_nodes_screen", return_value=False):
|
||||
mock_installer.on_prev()
|
||||
assert mock_installer.pages[mock_installer.page_index][0] == "init_cluster"
|
||||
|
||||
# network_scan -> deps_summary
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import sys
|
||||
from contextlib import ExitStack
|
||||
from unittest.mock import patch, MagicMock
|
||||
import pytest
|
||||
from pathlib import Path
|
||||
@ -24,33 +25,24 @@ def mock_installer(tmp_path):
|
||||
cfg_path = tmp_path / "prole.cfg"
|
||||
cfg_path.write_text(test_cfg_src.read_text())
|
||||
|
||||
with patch("install.tk.Tk"), patch("install.tk.Frame"), patch(
|
||||
"install.tk.Canvas"
|
||||
), patch("install.tk.Label"), patch("install.tk.Button"), patch(
|
||||
"install.tk.Entry"
|
||||
) as mock_entry, patch(
|
||||
"install.tk.StringVar"
|
||||
), patch(
|
||||
"install.tk.BooleanVar"
|
||||
), patch(
|
||||
"install.tk.IntVar"
|
||||
), patch(
|
||||
"install.ttk.Style"
|
||||
), patch(
|
||||
"install.Path.exists", return_value=True
|
||||
), patch(
|
||||
"install.Path.mkdir"
|
||||
), patch.object(
|
||||
ProleInstaller, "_save_env_to_file"
|
||||
), patch.object(
|
||||
ProleInstaller, "_save_prole_cfg"
|
||||
), patch.object(
|
||||
ProleInstaller, "_save_ansible_prole_vault"
|
||||
), patch.object(
|
||||
ProleInstaller, "_after_env_saved"
|
||||
), patch(
|
||||
"threading.Thread"
|
||||
):
|
||||
with ExitStack() as stack:
|
||||
stack.enter_context(patch("install.tk.Tk"))
|
||||
stack.enter_context(patch("install.tk.Frame"))
|
||||
stack.enter_context(patch("install.tk.Canvas"))
|
||||
stack.enter_context(patch("install.tk.Label"))
|
||||
stack.enter_context(patch("install.tk.Button"))
|
||||
mock_entry = stack.enter_context(patch("install.tk.Entry"))
|
||||
stack.enter_context(patch("install.tk.StringVar"))
|
||||
stack.enter_context(patch("install.tk.BooleanVar"))
|
||||
stack.enter_context(patch("install.tk.IntVar"))
|
||||
stack.enter_context(patch("install.ttk.Style"))
|
||||
stack.enter_context(patch("install.Path.exists", return_value=True))
|
||||
stack.enter_context(patch("install.Path.mkdir"))
|
||||
stack.enter_context(patch.object(ProleInstaller, "_save_env_to_file"))
|
||||
stack.enter_context(patch.object(ProleInstaller, "_save_prole_cfg"))
|
||||
stack.enter_context(patch.object(ProleInstaller, "_save_ansible_prole_vault"))
|
||||
stack.enter_context(patch.object(ProleInstaller, "_after_env_saved"))
|
||||
stack.enter_context(patch("threading.Thread"))
|
||||
|
||||
mock_entry.return_value.get.return_value.strip.return_value = "mock_val"
|
||||
root = MagicMock()
|
||||
@ -93,7 +85,7 @@ def test_init_cluster_next_goes_to_cluster_nodes(mock_installer):
|
||||
assert mock_installer.pages[mock_installer.page_index][0] == "cluster_nodes"
|
||||
|
||||
|
||||
def test_cluster_nodes_next_proceeds_to_database_options(mock_installer):
|
||||
def test_cluster_nodes_next_proceeds_to_common_services(mock_installer):
|
||||
mock_installer.show_page("cluster_nodes")
|
||||
mock_installer._cluster_nodes_current_policy = {
|
||||
"hosts": {},
|
||||
@ -106,7 +98,7 @@ def test_cluster_nodes_next_proceeds_to_database_options(mock_installer):
|
||||
return_value=True,
|
||||
):
|
||||
mock_installer.on_next()
|
||||
assert mock_installer.pages[mock_installer.page_index][0] == "database_options"
|
||||
assert mock_installer.pages[mock_installer.page_index][0] == "common_services"
|
||||
|
||||
|
||||
def test_cluster_nodes_prev_goes_back_to_cluster_environment(mock_installer):
|
||||
|
||||
Loading…
Reference in New Issue
Block a user