diff --git a/conf/prole.cfg b/conf/prole.cfg index e85115b..018f9cf 100644 --- a/conf/prole.cfg +++ b/conf/prole.cfg @@ -1,5 +1,5 @@ ; Prole Master Configuration File -; Generated by install.py on 2026-03-07 03:07:50 +; Generated by install.py on 2026-03-07 10:32:42 ; This file is used as input for Ansible deployment and k8s cluster creation. [User] @@ -10,7 +10,7 @@ PROLE_DATA = /Users/chrisfu/.prole/data PROLE_HOME = /Users/chrisfu/dev/prole PROLE_LOGS = /opt/prole/logs/chrisfu PROLE_SERVICE = ${PROLE_HOME}/etc -SERVICE_NAMESPACE = default +SERVICE_NAMESPACE = knoe-system [Inputs] ; Screen-scoped inputs used for unattended replays (-S) @@ -42,7 +42,7 @@ dependencies.opentofu.install = true dependencies.python.install = true dependencies.verify_all = false disk_selection.disk_type = local -disk_selection.local_path = /Users/chrisfu +disk_selection.local_path = ${PROLE_HOME}/prole-tools-app/dist disk_selection.removable_mount = env_setup.NAMESPACE = ${NAMESPACE} env_setup.PROLE_CONF = ${PROLE_CONF} @@ -51,13 +51,13 @@ env_setup.PROLE_HOME = ${PROLE_HOME} env_setup.PROLE_LOGS = ${PROLE_LOGS} env_setup.PROLE_SERVICE = ${PROLE_SERVICE} init_cluster.at_rest_encryption_enabled = true -init_cluster.cluster_env = dev -init_cluster.deployment_target = prole-dev-cluster +init_cluster.cluster_env = service +init_cluster.deployment_target = prole-service-cluster init_cluster.gitops_enabled = false init_cluster.k3s_server_url = -init_cluster.k3s_token = -init_cluster.kerberos_enabled = false -init_cluster.mode = k3d +init_cluster.k3s_token = ${PROLE_SECRET:v1:38WOjNObQUt_S1W_:q7cZ6C5FcsiNPfxtmUI5RtsuJ4iaBAdvLbw_1hpOx87Cvv2KL-HjbdE7s5J4aPegFw02IrwWuPP5WXe5nGkg7uTXreCsvg5uWYXc1vxjWHUzKEwVfcP2nPEKBjJrRuai1VxD8WaPbwcUMQfXEtqNrFrkBSIICYltM1hmmA==} +init_cluster.kerberos_enabled = true +init_cluster.mode = k3s init_cluster.start_cluster = true init_cluster.supabase_enabled = false init_cnpg_deploy.force_rollout = false @@ -70,10 +70,10 @@ init_password.db_password_confirm = init_password.db_username = root init_password.generate_ssh_key = true init_scripts.run_scripts = true -kerberos_config.enabled = false -kerberos_config.kdc = -kerberos_config.password = -kerberos_config.realm = +kerberos_config.enabled = true +kerberos_config.kdc = 10.0.0.3 +kerberos_config.password = ${PROLE_SECRET:v1:q3AVwS1x8Xr8V6n9:IRDXFhRi8uBOZvDmLoT-zKVjwGsiiDQvB4c430CqJS1PWz3X} +kerberos_config.realm = PROLE.ORG kerberos_config.test_connection = false kerberos_config.user = administrator network_scan.run = true @@ -83,18 +83,18 @@ ollama_config.server_port = 11434 [Global] ; Variables used by name in more than one place or assumed global scope -CLUSTER_ENV = dev +CLUSTER_ENV = service DB_HOST_PORT = 5432 DB_PASSWORD = -DEPLOYMENT_MODE = k3d -DEPLOYMENT_TARGET = prole-dev-cluster +DEPLOYMENT_MODE = k3s +DEPLOYMENT_TARGET = prole-service-cluster NAMESPACE = ${NAMESPACE} PROLE_DB_USER = root PROLE_HOME = ${PROLE_HOME} PROLE_K3S_SERVER = -PROLE_K3S_TOKEN = +PROLE_K3S_TOKEN = ${PROLE_SECRET:v1:38WOjNObQUt_S1W_:q7cZ6C5FcsiNPfxtmUI5RtsuJ4iaBAdvLbw_1hpOx87Cvv2KL-HjbdE7s5J4aPegFw02IrwWuPP5WXe5nGkg7uTXreCsvg5uWYXc1vxjWHUzKEwVfcP2nPEKBjJrRuai1VxD8WaPbwcUMQfXEtqNrFrkBSIICYltM1hmmA==} PROLE_OPENTOFU_URL = http://127.0.0.1:8080 -SERVICE_NAMESPACE = default +SERVICE_NAMESPACE = knoe-system [Welcome] ; No configuration values captured yet for this section. @@ -111,6 +111,8 @@ ANSIBLE_INVENTORY = ${PROLE_HOME}/infrastructure/inventory ANSIBLE_REALM = PROLE.ORG ANSIBLE_TOPOLOGY = {"domain":"prole.org","realm":"PROLE.ORG","internal_records":{"aventage.prole.org":"10.0.0.206","fairyland.prole.org":"10.0.0.208","loghost.prole.org":"10.0.0.3","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":"","server_host":"","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"],"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.3 +KERBEROS_AUTO_ENABLED = True [Port Forwards] ; No configuration values captured yet for this section. @@ -132,7 +134,10 @@ PROLE_SERVICE = ${PROLE_SERVICE} ; No configuration values captured yet for this section. [Optional Features] -; No configuration values captured yet for this section. +AT_REST_ENCRYPTION_ENABLED = True +GITOPS_ENABLED = False +KERBEROS_ENABLED = True +SUPABASE_ENABLED = False [GitOps] ; No configuration values captured yet for this section. @@ -141,24 +146,26 @@ PROLE_SERVICE = ${PROLE_SERVICE} NAMESPACE = ${NAMESPACE} [Initialize Cluster] -; No configuration values captured yet for this section. +ENVIRONMENT = service +K3S_SERVER_URL = +K3S_TOKEN = ${PROLE_SECRET:v1:38WOjNObQUt_S1W_:q7cZ6C5FcsiNPfxtmUI5RtsuJ4iaBAdvLbw_1hpOx87Cvv2KL-HjbdE7s5J4aPegFw02IrwWuPP5WXe5nGkg7uTXreCsvg5uWYXc1vxjWHUzKEwVfcP2nPEKBjJrRuai1VxD8WaPbwcUMQfXEtqNrFrkBSIICYltM1hmmA==} [Dev Cluster (k3d)] CLUSTER_ENV = dev DISPLAY_NAME = knoe-dev-cluster -KUBECTL_CONTEXT = +KUBECTL_CONTEXT = prole-k3s MODE = k3d [Service Cluster (k3s)] CLUSTER_ENV = prole-service-cluster DISPLAY_NAME = prole-service-cluster K3S_SERVER_URL = -K3S_TOKEN = +K3S_TOKEN = ${PROLE_SECRET:v1:38WOjNObQUt_S1W_:q7cZ6C5FcsiNPfxtmUI5RtsuJ4iaBAdvLbw_1hpOx87Cvv2KL-HjbdE7s5J4aPegFw02IrwWuPP5WXe5nGkg7uTXreCsvg5uWYXc1vxjWHUzKEwVfcP2nPEKBjJrRuai1VxD8WaPbwcUMQfXEtqNrFrkBSIICYltM1hmmA==} MODE = k3s PIPELINE_URL = http://127.0.0.1:8080 [Prod Cluster (k8s)] -ARTIFACTS_DIR = /private/var/folders/rt/pywlnmxj3dn7t5552vwdcpp80000gn/T/pytest-of-chrisfu/pytest-132/test_encryption_toggle0/data/staging +ARTIFACTS_DIR = /Users/chrisfu/dev/prole/data/staging CLUSTER_ENV = prole-prod-cluster DISPLAY_NAME = prole-prod-cluster MODE = k8s @@ -171,8 +178,8 @@ PIPELINE_URL = http://127.0.0.1:8080 ; No configuration values captured yet for this section. [Deployment] -MODE = k3d -TARGET = prole-dev-cluster +MODE = k3s +TARGET = prole-service-cluster [Install] ; No configuration values captured yet for this section. diff --git a/etc/init_certmgr.sh b/etc/init_certmgr.sh index 7e7c047..42152c0 100755 --- a/etc/init_certmgr.sh +++ b/etc/init_certmgr.sh @@ -48,7 +48,7 @@ fi ACTION="$COMMON_CORE_ACTION" # cert-manager always runs in its own dedicated namespace; never follows the -# common-services namespace override. +# Registry/service namespace override. CERTMGR_NAMESPACE="cert-manager" common_core_apply_namespace "$CERTMGR_NAMESPACE" diff --git a/etc/init_cloudnative_pg.sh b/etc/init_cloudnative_pg.sh index c8d3646..e0234be 100755 --- a/etc/init_cloudnative_pg.sh +++ b/etc/init_cloudnative_pg.sh @@ -724,7 +724,7 @@ _push_to_k3s_registry() { # Fallback: port-forward the in-cluster registry service and push via localhost. # This avoids relying on hostPort / firewall rules for ${push_host}. if command -v kubectl >/dev/null 2>&1 && command -v skopeo >/dev/null 2>&1; then - local reg_ns="${REGISTRY_NAMESPACE:-${COMMON_SERVICES_NAMESPACE:-common-services}}" + local reg_ns="${REGISTRY_NAMESPACE:-${SERVICE_NAMESPACE:-${NAMESPACE:-default}}}" local pf_port="${PROLE_REGISTRY_PORT_FORWARD_LOCAL:-55000}" local pf_log pf_log="$(mktemp -t prole-registry-pf.XXXXXX)" diff --git a/etc/init_common_services.sh b/etc/init_common_services.sh index f175953..1265e60 100755 --- a/etc/init_common_services.sh +++ b/etc/init_common_services.sh @@ -87,7 +87,8 @@ if [ -z "$NS" ]; then fi ARGOCD_NS="${ARGOCD_NAMESPACE:-argocd}" -REGISTRY_NS="${REGISTRY_NAMESPACE:-${COMMON_SERVICES_NAMESPACE:-common-services}}" +# Registry should live in the common-core/service namespace unless explicitly overridden. +REGISTRY_NS="${REGISTRY_NAMESPACE:-${NS}}" prole_ensure_kubeconfig >/dev/null 2>&1 || true echo "DEBUG: prole_ensure_kubeconfig finished" >&2 @@ -392,7 +393,7 @@ _preload_docker_images() { if [[ -z "$registry" ]]; then echo " [WARN] Could not resolve registry host (set LOCAL_REGISTRY or PROLE_K3S_SERVER)." >&2 echo " Images will be pulled at deploy time." >&2 - ;; + return 0 fi _registry_ref_exists() { @@ -413,7 +414,7 @@ _preload_docker_images() { if ! curl -fsS "http://${registry}/v2/" >/dev/null 2>&1; then echo " [WARN] Registry not reachable at http://${registry}/v2/." >&2 echo " Images will be pulled at deploy time." >&2 - ;; + return 0 fi local tar_file diff --git a/etc/init_k3s_registry.sh b/etc/init_k3s_registry.sh index 8089829..aa182ff 100644 --- a/etc/init_k3s_registry.sh +++ b/etc/init_k3s_registry.sh @@ -24,7 +24,7 @@ registry_host_from_url() { K3S_REGISTRY_HOST=${K3S_REGISTRY_HOST:-$(registry_host_from_url "${PROLE_K3S_SERVER:-${K3S_SERVER_URL:-}}")} K3S_REGISTRY_PORT=${K3S_REGISTRY_PORT:-5000} -K3S_REGISTRY_NAMESPACE=${K3S_REGISTRY_NAMESPACE:-${REGISTRY_NAMESPACE:-${COMMON_SERVICES_NAMESPACE:-common-services}}} +K3S_REGISTRY_NAMESPACE=${K3S_REGISTRY_NAMESPACE:-${REGISTRY_NAMESPACE:-${SERVICE_NAMESPACE:-${PROLE_NAMESPACE:-default}}}} K3S_REGISTRY_FILE=${K3S_REGISTRY_FILE:-/etc/rancher/k3s/registries.yaml} ensure_root() { diff --git a/etc/init_kdc.sh b/etc/init_kdc.sh index 19515a9..ef9687a 100755 --- a/etc/init_kdc.sh +++ b/etc/init_kdc.sh @@ -56,15 +56,31 @@ KRB5_KDC=${KRB5_KDC:-} KRB5_ADMIN=${KRB5_ADMIN:-} local_registry_enabled() { - case "${PROLE_ENABLE_LOCAL_REGISTRY:-${ENABLE_LOCAL_REGISTRY:-}}" in - 1|true|TRUE|True|yes|YES|Yes|on|ON|On) return 0 ;; - esac - return 1 + local raw="${PROLE_ENABLE_LOCAL_REGISTRY:-${ENABLE_LOCAL_REGISTRY:-}}" + if [[ -n "$raw" ]]; then + case "$raw" in + 1|true|TRUE|True|yes|YES|Yes|on|ON|On) return 0 ;; + esac + return 1 + fi + + local mode="" + if command -v prole_normalize_mode >/dev/null 2>&1; then + mode=$(prole_normalize_mode "${PROLE_MODE:-${DEPLOYMENT_MODE:-${CLUSTER_ENV:-}}}") + else + mode="${PROLE_MODE:-${DEPLOYMENT_MODE:-${CLUSTER_ENV:-}}}" + fi + [[ "$mode" == "k3d" || "$mode" == "k3s" ]] } if ! local_registry_enabled; then - PROLE_KDC_REGISTRY_HOST="" + # Only clear default local-registry settings; preserve explicit registry host values. + _default_host="${LOCAL_REGISTRY:-localhost:5000}" + if [[ -z "${PROLE_KDC_REGISTRY_HOST:-}" || "${PROLE_KDC_REGISTRY_HOST:-}" == "$_default_host" ]]; then + PROLE_KDC_REGISTRY_HOST="" + fi PROLE_KDC_REGISTRY_INTERNAL="" + unset _default_host fi log() { printf '%s\n' "$*"; } @@ -264,7 +280,7 @@ resolve_prole_kdc_defaults() { fi fi if [[ -z "$PROLE_KDC_REGISTRY_INTERNAL" ]]; then - local reg_ns="${REGISTRY_NAMESPACE:-${COMMON_SERVICES_NAMESPACE:-common-services}}" + local reg_ns="${REGISTRY_NAMESPACE:-${SERVICE_NAMESPACE:-${NAMESPACE:-default}}}" PROLE_KDC_REGISTRY_INTERNAL="registry.${reg_ns}.svc.cluster.local:5000" unset reg_ns fi diff --git a/etc/init_registry.sh b/etc/init_registry.sh index b0e2d40..fcfd1b0 100755 --- a/etc/init_registry.sh +++ b/etc/init_registry.sh @@ -136,7 +136,8 @@ if [[ -n "$REGISTRY_NAMESPACE_OVERRIDE" ]]; then elif [[ -n "${REGISTRY_NAMESPACE:-}" ]]; then REGISTRY_NAMESPACE="$REGISTRY_NAMESPACE" else - REGISTRY_NAMESPACE="${COMMON_SERVICES_NAMESPACE:-common-services}" + # Default to the configured service namespace (driven by prole.cfg via prole_cfg.sh). + REGISTRY_NAMESPACE="${SERVICE_NAMESPACE:-${PROLE_NAMESPACE:-default}}" fi # In k3s mode, the in-cluster registry is a common core service and should live @@ -146,7 +147,7 @@ if declare -F prole_normalize_mode >/dev/null 2>&1; then _mode_resolved="$(prole_normalize_mode "$_mode_resolved")" fi if [[ "$_mode_resolved" == "k3s" && ( -z "${REGISTRY_NAMESPACE:-}" || "${REGISTRY_NAMESPACE}" == "default" ) ]]; then - REGISTRY_NAMESPACE="${COMMON_SERVICES_NAMESPACE:-common-services}" + REGISTRY_NAMESPACE="${SERVICE_NAMESPACE:-${PROLE_NAMESPACE:-default}}" fi unset _mode_resolved diff --git a/etc/init_service_layer.sh b/etc/init_service_layer.sh index 714b522..6e8f380 100755 --- a/etc/init_service_layer.sh +++ b/etc/init_service_layer.sh @@ -116,7 +116,8 @@ fi SERVICE_LAYER_NAMESPACE="$SERVICE_NAMESPACE" ARGOCD_NS="${ARGOCD_NAMESPACE:-argocd}" -REGISTRY_NS="${REGISTRY_NAMESPACE:-${COMMON_SERVICES_NAMESPACE:-common-services}}" +# Registry should live in the service-layer namespace unless explicitly overridden. +REGISTRY_NS="${REGISTRY_NAMESPACE:-${SERVICE_NAMESPACE}}" STATE_DIR="" if [[ -n "${PROLE_SERVICE:-}" && -f "$PROLE_SERVICE/prole_cfg.sh" ]]; then diff --git a/etc/prole_cfg.sh b/etc/prole_cfg.sh index 840e0c4..63b1fd7 100644 --- a/etc/prole_cfg.sh +++ b/etc/prole_cfg.sh @@ -314,7 +314,8 @@ if [[ -n "${PROLE_NAMESPACE:-}" ]]; then esac case "$_prole_mode_resolved" in k3s|k8s) - _prole_registry_ns="${REGISTRY_NAMESPACE:-${COMMON_SERVICES_NAMESPACE:-common-services}}" + # Registry namespace is driven by config (SERVICE_NAMESPACE) unless explicitly overridden. + _prole_registry_ns="${REGISTRY_NAMESPACE:-${SERVICE_NAMESPACE:-${PROLE_NAMESPACE:-}}}" if [[ -n "${LOCAL_REGISTRY_INTERNAL:-}" ]]; then : elif _prole_local_registry_enabled; then diff --git a/infrastructure/roles/k3s/tasks/install.yml b/infrastructure/roles/k3s/tasks/install.yml index bcd6c0b..61ced6e 100644 --- a/infrastructure/roles/k3s/tasks/install.yml +++ b/infrastructure/roles/k3s/tasks/install.yml @@ -381,7 +381,7 @@ k3s_registry_namespace_resolved: >- {{ (k3s_registry_namespace | default('')) if (k3s_registry_namespace | default('') | length > 0 and k3s_registry_namespace != '${SERVICE_NAMESPACE}') else (k3s_prole_service_namespace_cfg | default('')) if (k3s_prole_service_namespace_cfg | default('') | length > 0 and k3s_prole_service_namespace_cfg != '${SERVICE_NAMESPACE}') - else 'common-services' }} + else 'knoe-system' }} when: k3s_state == "present" and k3s_registry_config_enabled | bool - name: Validate k3s registry host diff --git a/infrastructure/roles/k3s/tasks/sync.yml b/infrastructure/roles/k3s/tasks/sync.yml index 5387118..6194c6f 100644 --- a/infrastructure/roles/k3s/tasks/sync.yml +++ b/infrastructure/roles/k3s/tasks/sync.yml @@ -73,7 +73,7 @@ k3s_registry_namespace_resolved: >- {{ (k3s_registry_namespace | default('')) if (k3s_registry_namespace | default('') | length > 0 and k3s_registry_namespace != '${SERVICE_NAMESPACE}') else (k3s_prole_service_namespace_cfg | default('')) if (k3s_prole_service_namespace_cfg | default('') | length > 0 and k3s_prole_service_namespace_cfg != '${SERVICE_NAMESPACE}') - else 'common-services' }} + else 'knoe-system' }} when: k3s_registry_config_enabled | bool - name: Validate k3s registry host diff --git a/installer/core/actions.py b/installer/core/actions.py index 93a5914..4dda629 100644 --- a/installer/core/actions.py +++ b/installer/core/actions.py @@ -1971,7 +1971,11 @@ class ProleSilentInstaller(ProleInstallerBase): ) if not ns: ns = (os.environ.get("REGISTRY_NAMESPACE") or "").strip() - return ns or "common-services" + if ns: + return ns + + # Registry is a common-core service; default to the configured service namespace. + return self._service_namespace() def _ensure_registry_defaults(self, mode: str) -> None: db = self.prole_cfg_data.get("Docker Build", {}) or {} diff --git a/installer/core/env.py b/installer/core/env.py index 2312f2d..00bc25a 100644 --- a/installer/core/env.py +++ b/installer/core/env.py @@ -386,10 +386,6 @@ def _verify_k3s_services_status( mapped to status strings (``Good``, ``Failing``, ``Error``, …). """ status = {"registry": "Unknown", "openbao": "Unknown", "opentofu": "Unknown"} - server, token = _resolve_k3s_connection(project_root=project_root) - if not token: - return {k: "Missing token" for k in status} - base_cmd = _kubectl_base_cmd_for_k3s( managed_kubeconfig=managed_kubeconfig, project_root=project_root ) @@ -400,6 +396,10 @@ def _verify_k3s_services_status( text=True, timeout=15, ) + if res.returncode != 0: + # Avoid reporting a misleading "Missing token" when the user is + # authenticated via kubeconfig/context instead of K3S_TOKEN. + return {k: "Error" for k in status} out = res.stdout.lower() if res.stdout else "" status["registry"] = "Good" if "registry" in out else "Failing" status["openbao"] = "Good" if ("openbao" in out or "bao" in out) else "Failing" diff --git a/installer/ui/screens/cluster.py b/installer/ui/screens/cluster.py index d2236d6..f7cd6ba 100644 --- a/installer/ui/screens/cluster.py +++ b/installer/ui/screens/cluster.py @@ -215,14 +215,20 @@ class ClusterScreenMixin: ) combo.bind("<>", self._on_kubectx_select) if not self.selected_kubectx.get() and values: - if selected_env_key == "service" and "prole-k3s" in values: + # Default to the current context if it exists in the list; otherwise pick + # a sensible default without switching immediately. + current_ctx = (getattr(self, "_kubectx_applied", "") or "").strip() + if not current_ctx: + current_ctx = (self._current_kubectl_context() or "").strip() + if current_ctx: + self._kubectx_applied = current_ctx + + if current_ctx and current_ctx in values: + self.selected_kubectx.set(current_ctx) + elif selected_env_key == "service" and "prole-k3s" in values: self.selected_kubectx.set("prole-k3s") - # Auto-switch to prole-k3s if we just loaded the page and it's selected - self._switch_kubectx("prole-k3s") else: self.selected_kubectx.set(values[0]) - # No auto-switch for others to avoid surprises, but we should probably - # if it's the only one. combo_win = self.bg_canvas.create_window( x_label + 190, y - 6, window=combo, anchor="nw" ) @@ -239,6 +245,30 @@ class ClusterScreenMixin: fill="#6e6e73", font=("SF Pro Text", 10), ) + + apply_btn = tk.Button( + self.bg_canvas, + text="Apply", + command=self._on_kubectx_apply, + bg="#F5F5DC", + fg="black", + activebackground="#E5E5D5", + highlightbackground="#F5F5DC", + highlightthickness=0, + relief="flat", + font=("SF Pro Text", 10), + padx=10, + state="disabled", + ) + apply_win = self.bg_canvas.create_window( + x_label + 670, y - 10, window=apply_btn, anchor="nw" + ) + self._canvas_items.append(apply_win) + self._overlay_widgets.append(apply_btn) + # Keep references for UI tests / layout verification. + self._kubectx_apply_btn = apply_btn + self._kubectx_apply_btn_canvas_window = apply_win + self._update_kubectx_apply_button() y += 34 if selected_env_key == "prod": @@ -542,10 +572,14 @@ class ClusterScreenMixin: self._apply_k3s_defaults() except Exception: pass - # Auto-switch to prole-k3s if it exists - if hasattr(self, "selected_kubectx"): + # Prefer prole-k3s context if present, but require explicit Apply. + try: + values = self._get_kubectx_list() + except Exception: + values = [] + if hasattr(self, "selected_kubectx") and "prole-k3s" in (values or []): self.selected_kubectx.set("prole-k3s") - self._switch_kubectx("prole-k3s") + self._update_kubectx_apply_button() elif env_key == "dev": name = self.selected_k3d_cluster.get() if name: @@ -1128,46 +1162,132 @@ class ClusterScreenMixin: def _on_kubectx_select(self, *args): """Called when a generic kubernetes context is selected from the dropdown.""" - ctx = self.selected_kubectx.get() + self._update_kubectx_apply_button() + ctx = (self.selected_kubectx.get() or "").strip() if ctx: - self._switch_kubectx(ctx) - self._verify_k3s_services() - self.show_page("init_cluster") + self._set_cluster_env_message( + f"Context change pending: {ctx} (click Apply)", + "#ff9500", + clear_after_ms=4000, + ) - def _switch_kubectx(self, context_name: str): + def _on_kubectx_apply(self): + ctx = (self.selected_kubectx.get() or "").strip() + if not ctx: + return + ok = self._switch_kubectx(ctx) + if ok: + self._kubectx_applied = ctx + self._update_kubectx_apply_button() + self._set_cluster_env_message( + f"Context applied: {ctx}", "#34c759", clear_after_ms=3000 + ) + self._verify_k3s_services() + self.show_page("init_cluster") + + def _update_kubectx_apply_button(self): + btn = getattr(self, "_kubectx_apply_btn", None) + if not btn: + return + selected = "" + try: + if hasattr(self, "selected_kubectx") and self.selected_kubectx is not None: + selected = (self.selected_kubectx.get() or "").strip() + except Exception: + selected = "" + applied = (getattr(self, "_kubectx_applied", "") or "").strip() + state = "normal" if (selected and selected != applied) else "disabled" + try: + btn.config(state=state) + except Exception: + pass + + def _switch_kubectx(self, context_name: str) -> bool: """Switch the current kubernetes context using kubectx or kubectl.""" if not context_name: - return - + return False + # Determine which kubeconfig to modify. # Use the same logic as the rest of the application. kc = _find_kubeconfig_file() env = os.environ.copy() if kc: env["KUBECONFIG"] = kc - + try: # Try kubectx first res = subprocess.run( ["kubectx", context_name], capture_output=True, text=True, env=env ) - if res.returncode != 0: + ok = res.returncode == 0 + err = (res.stderr or "").strip() + if not ok: # Fallback to kubectl config use-context - subprocess.run( + res2 = subprocess.run( ["kubectl", "config", "use-context", context_name], capture_output=True, text=True, env=env ) + ok = res2.returncode == 0 + if not ok: + err2 = (res2.stderr or "").strip() + err = err2 or err # Update the UI dropdown to match (but don't trigger re-select) - if hasattr(self, "selected_kubectx") and self.selected_kubectx.get() != context_name: - # Check if it's in the current values list - current_values = list(self.kubectx_list.get() or []) - if context_name in current_values: - self.selected_kubectx.set(context_name) + if ( + ok + and hasattr(self, "selected_kubectx") + and self.selected_kubectx.get() != context_name + ): + try: + # Check if it's in the current values list + current_values = [] + if hasattr(self, "kubectx_list") and self.kubectx_list is not None: + current_values = list(self.kubectx_list.get() or []) + if not current_values: + current_values = self._get_kubectx_list() + if context_name in (current_values or []): + self.selected_kubectx.set(context_name) + except Exception: + pass + + if not ok: + try: + messagebox.showerror( + "Kubernetes Context", + f"Failed to switch to context '{context_name}'.\n\n{err or 'Unknown error'}", + ) + except Exception: + pass + return ok + except Exception as e: + try: + messagebox.showerror( + "Kubernetes Context", + f"Failed to switch to context '{context_name}': {e}", + ) + except Exception: + pass + return False + + def _current_kubectl_context(self) -> str: + kc = _find_kubeconfig_file() + env = os.environ.copy() + if kc: + env["KUBECONFIG"] = kc + try: + res = subprocess.run( + ["kubectl", "config", "current-context"], + capture_output=True, + text=True, + env=env, + ) + if res.returncode == 0: + return (res.stdout or "").strip() except Exception: pass + return "" def _get_kubectx_list(self) -> list[str]: """Get list of kubernetes contexts.""" @@ -1197,6 +1317,15 @@ class ClusterScreenMixin: def _validate_and_save_cluster_config(self) -> bool: """Verify the config then write the values to prole.cfg.""" env_key = self._cluster_env_key() + prev_service_ns = "" + try: + prev_service_ns = ( + (self.prole_cfg_data.get("Global", {}) or {}) + .get("SERVICE_NAMESPACE", "") + .strip() + ) + except Exception: + prev_service_ns = "" if env_key == "dev": cluster_val = self.selected_k3d_cluster.get() or "knoe-dev-cluster" if not cluster_val: @@ -1215,14 +1344,19 @@ class ClusterScreenMixin: except Exception: pass elif env_key == "service": - if not (self.k3s_server_url.get() or "").strip(): + # Service deployments can authenticate via an existing kubeconfig/context + # (preferred), or via a token-based generated kubeconfig. + kubeconfig = _find_kubeconfig_file() + has_token_flow = bool((self.k3s_server_url.get() or "").strip()) and bool( + (self.k3s_token.get() or "").strip() + ) + if not kubeconfig and not has_token_flow: messagebox.showwarning( - "Validation", "Please specify the K3s server URL." + "Validation", + "Missing cluster credentials. Please select a working Kubernetes context (kubectx) " + "or provide K3s server URL + token.", ) return False - if not (self.k3s_token.get() or "").strip(): - messagebox.showwarning("Validation", "Please specify the K3s token.") - return False cluster_val = self.cluster_env.get() elif env_key == "prod": path = self.prod_artifacts_path.get().strip() @@ -1255,6 +1389,7 @@ class ClusterScreenMixin: self.prole_cfg_data["Global"][ "SERVICE_NAMESPACE" ] = self._get_service_namespace() + new_service_ns = self.prole_cfg_data["Global"]["SERVICE_NAMESPACE"] self.prole_cfg_data["Optional Features"]["SUPABASE_ENABLED"] = str( self.supabase_enabled.get() ) @@ -1275,6 +1410,62 @@ class ClusterScreenMixin: # Save config self._save_prole_cfg() + + # Namespace change cleanup (prevents collisions when switching namespaces). + try: + prev = (prev_service_ns or "").strip() + new = (new_service_ns or "").strip() + if prev and new and prev != new: + do_cleanup = False + try: + do_cleanup = messagebox.askyesno( + "Namespace Changed", + ( + f"Common Core Services namespace changed from '{prev}' to '{new}'.\n\n" + f"Reset the OLD namespace '{prev}' to avoid resource collisions?\n\n" + "This deletes workloads/services in that namespace (PVCs are preserved)." + ), + ) + except Exception: + do_cleanup = False + + if do_cleanup: + + def _cleanup_worker(): + try: + self._set_cluster_env_message( + f"Resetting old namespace: {prev}", + "#ff9500", + clear_after_ms=3000, + ) + try: + self._ensure_k3s_kubeconfig_merged() + except Exception: + pass + server, token = ("", "") + try: + server, token = self._k3s_connection_info() + except Exception: + server, token = ("", "") + controller = getattr(self, "controller", None) + project_root = getattr(controller, "project_root", None) + if project_root: + _reset_k3s_namespace(project_root, prev, server, token) + self._set_cluster_env_message( + f"Old namespace reset complete: {prev}", + "#34c759", + clear_after_ms=4000, + ) + except Exception as e: + self._set_cluster_env_message( + f"Namespace reset failed: {e}", + "#ff3b30", + clear_after_ms=6000, + ) + + threading.Thread(target=_cleanup_worker, daemon=True).start() + except Exception: + pass return True def _cluster_status_snapshot(self) -> dict: diff --git a/installer/ui/screens/services.py b/installer/ui/screens/services.py index 3161d36..1280b5d 100644 --- a/installer/ui/screens/services.py +++ b/installer/ui/screens/services.py @@ -1594,7 +1594,7 @@ class ServicesScreenMixin: # Pre-pull dependent images into local registry before initialization if mode == "k3s": console.write( - "[INFO] Skipping image pre-pull in k3s mode (images are handled by k3s/common-services import).\n" + "[INFO] Skipping image pre-pull in k3s mode (images are handled by the in-cluster registry/import).\n" ) else: try: diff --git a/mock_val/init_certmgr.sh b/mock_val/init_certmgr.sh index 7e7c047..42152c0 100755 --- a/mock_val/init_certmgr.sh +++ b/mock_val/init_certmgr.sh @@ -48,7 +48,7 @@ fi ACTION="$COMMON_CORE_ACTION" # cert-manager always runs in its own dedicated namespace; never follows the -# common-services namespace override. +# Registry/service namespace override. CERTMGR_NAMESPACE="cert-manager" common_core_apply_namespace "$CERTMGR_NAMESPACE" diff --git a/mock_val/init_common_services.sh b/mock_val/init_common_services.sh index 12233fc..00c13e3 100755 --- a/mock_val/init_common_services.sh +++ b/mock_val/init_common_services.sh @@ -86,7 +86,7 @@ if [ -z "$NS" ]; then fi ARGOCD_NS="${ARGOCD_NAMESPACE:-argocd}" -REGISTRY_NS="${REGISTRY_NAMESPACE:-${COMMON_SERVICES_NAMESPACE:-common-services}}" +REGISTRY_NS="${REGISTRY_NAMESPACE:-${NS}}" prole_ensure_kubeconfig >/dev/null 2>&1 || true diff --git a/mock_val/init_kdc.sh b/mock_val/init_kdc.sh index 8718969..0591346 100755 --- a/mock_val/init_kdc.sh +++ b/mock_val/init_kdc.sh @@ -263,7 +263,7 @@ resolve_prole_kdc_defaults() { fi fi if [[ -z "$PROLE_KDC_REGISTRY_INTERNAL" ]]; then - local reg_ns="${REGISTRY_NAMESPACE:-${COMMON_SERVICES_NAMESPACE:-common-services}}" + local reg_ns="${REGISTRY_NAMESPACE:-${SERVICE_NAMESPACE:-${NAMESPACE:-default}}}" PROLE_KDC_REGISTRY_INTERNAL="registry.${reg_ns}.svc.cluster.local:5000" unset reg_ns fi diff --git a/tests/etc/test_init_kdc_k3s_registry_default.sh b/tests/etc/test_init_kdc_k3s_registry_default.sh new file mode 100644 index 0000000..cf0bf3e --- /dev/null +++ b/tests/etc/test_init_kdc_k3s_registry_default.sh @@ -0,0 +1,155 @@ +#!/usr/bin/env bash +# Regression test: etc/init_kdc.sh should treat local registry as enabled by default in k3s mode +# (matching etc/prole_cfg.sh defaults), so it must not fail with “No registry host configured for k3s”. + +set -euo pipefail + +SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) +PROLE_HOME=$(cd "$SCRIPT_DIR/../.." && pwd) +SOURCE_ETC_DIR="$PROLE_HOME/etc" + +TMP_DIR=$(mktemp -d) +trap 'rm -rf "$TMP_DIR"' EXIT + +WORK_DIR="$TMP_DIR/work" +BIN_DIR="$TMP_DIR/bin" +mkdir -p "$WORK_DIR/etc" "$WORK_DIR/conf" "$WORK_DIR/authority" "$BIN_DIR" + +cp "$SOURCE_ETC_DIR/init_kdc.sh" "$WORK_DIR/etc/init_kdc.sh" +cp "$SOURCE_ETC_DIR/prole_cfg.sh" "$WORK_DIR/etc/prole_cfg.sh" +chmod +x "$WORK_DIR/etc/init_kdc.sh" + +cat < "$WORK_DIR/env.sh" +#!/usr/bin/env bash +export PROLE_HOME="$WORK_DIR" +export PROLE_CONF="$WORK_DIR/conf" +EOF + +cat < "$WORK_DIR/conf/prole.cfg" +[globals] +prole.home = $WORK_DIR +prole.mode = k3s +NAMESPACE = prole-db-a0001 +EOF + +export KUBECTL_LOG="$TMP_DIR/kubectl.log" +export DOCKER_LOG="$TMP_DIR/docker.log" + +cat <<'EOF' > "$BIN_DIR/kubectl" +#!/usr/bin/env bash +set -euo pipefail + +args=("$@") +printf '%s\n' "${args[*]}" >> "${KUBECTL_LOG}" + +sub="${1:-}" +case "$sub" in + config) + case "${2:-}" in + current-context) + printf '%s' "docker-desktop" + exit 0 + ;; + use-context|get-contexts) + exit 0 + ;; + esac + exit 0 + ;; + get) + # For namespace existence checks, succeed. + if [[ "${2:-}" == "namespace" ]]; then + exit 0 + fi + # For deployment checks, report missing to exercise the deploy-time timeout path. + if [[ "${2:-}" == "deploy" || "${2:-}" == "deployment" ]]; then + exit 1 + fi + exit 0 + ;; + create) + # init_kdc.sh pipes YAML from create-secret into kubectl apply. + if [[ "${2:-}" == "secret" ]]; then + cat </dev/null || true + exit 0 + ;; + rollout|wait|exec|delete|describe|logs|patch) + exit 0 + ;; + *) + exit 0 + ;; +esac +EOF +chmod +x "$BIN_DIR/kubectl" + +cat <<'EOF' > "$BIN_DIR/docker" +#!/usr/bin/env bash +set -euo pipefail + +args=("$@") +printf '%s\n' "${args[*]}" >> "${DOCKER_LOG}" + +case "${1:-}" in + info|build|tag|push) + exit 0 + ;; +esac + +exit 0 +EOF +chmod +x "$BIN_DIR/docker" + +export PATH="$BIN_DIR:$PATH" + +export PROLE_HOME="$WORK_DIR" +export PROLE_CONF="$WORK_DIR/conf" +export PROLE_MODE="k3s" + +# Avoid secret generation paths (python/openssl) by providing explicit values. +export PROLE_KDC_ADMIN_PASSWORD="test-admin" +export PROLE_KDC_MASTER_PASSWORD="test-master" +export PROLE_KDC_TRUST_PASSWORD="test-trust" +export PROLE_KDC_TRUST_SHARED_PASSWORD="test-shared" + +if "$WORK_DIR/etc/init_kdc.sh" update >"$TMP_DIR/stdout" 2>"$TMP_DIR/stderr"; then + : +else + echo "FAILURE: init_kdc.sh update exited non-zero" + cat "$TMP_DIR/stderr" + exit 1 +fi + +if grep -q "No registry host configured for k3s" "$TMP_DIR/stderr"; then + echo "FAILURE: unexpected missing-registry-host error for k3s mode" + cat "$TMP_DIR/stderr" + exit 1 +fi + +if ! grep -q "push" "$DOCKER_LOG"; then + echo "FAILURE: expected docker push to be attempted" + cat "$DOCKER_LOG" + exit 1 +fi + +if ! grep -q "push localhost:5000/prole-authority:latest" "$DOCKER_LOG"; then + echo "FAILURE: expected docker push to default localhost registry in k3s mode" + cat "$DOCKER_LOG" + exit 1 +fi + +echo "SUCCESS" diff --git a/tests/installer/test_actions_helpers.py b/tests/installer/test_actions_helpers.py index d2cd56e..18ce2c3 100644 --- a/tests/installer/test_actions_helpers.py +++ b/tests/installer/test_actions_helpers.py @@ -324,10 +324,12 @@ class TestNamespaceHelpers: assert inst._registry_namespace() == "env-reg" def test_registry_default(self): - inst = _TestableSilentInstaller() + # Registry is a common-core service; default should follow SERVICE_NAMESPACE. + # For k3s/service clusters, SERVICE_NAMESPACE defaults to knoe-system. + inst = _TestableSilentInstaller(inputs={"init_cluster.cluster_env": "service"}) with mock.patch.dict(os.environ, {}, clear=False): os.environ.pop("REGISTRY_NAMESPACE", None) - assert inst._registry_namespace() == "common-services" + assert inst._registry_namespace() == "knoe-system" # --------------------------------------------------------------------------- diff --git a/tests/installer/test_cluster_screen_layout.py b/tests/installer/test_cluster_screen_layout.py index 8550233..9312d80 100644 --- a/tests/installer/test_cluster_screen_layout.py +++ b/tests/installer/test_cluster_screen_layout.py @@ -27,7 +27,7 @@ class _DummyClusterApp(ScreenBaseMixin, ClusterScreenMixin): self.cluster_env = tk.StringVar(master=root, value="prod") self.selected_k3d_cluster = tk.StringVar(master=root, value="") self.selected_kubectx = tk.StringVar(master=root, value="") - self.service_namespace = tk.StringVar(master=root, value="common-services") + self.service_namespace = tk.StringVar(master=root, value="knoe-system") self.prod_artifacts_path = tk.StringVar(master=root, value="/tmp") # Legacy variables still referenced by non-UI logic elsewhere @@ -35,6 +35,8 @@ class _DummyClusterApp(ScreenBaseMixin, ClusterScreenMixin): self.k3s_token = tk.StringVar(master=root, value="") self._kubectx_values = kubectx_values + # Avoid invoking real `kubectl` during layout tests. + self._kubectx_applied = kubectx_values[0] if kubectx_values else "" # ---- Stubs for callbacks / helpers invoked by rendering ---- def _on_cluster_env_change(self, *_args): @@ -56,7 +58,10 @@ class _DummyClusterApp(ScreenBaseMixin, ClusterScreenMixin): return None def _switch_kubectx(self, _context_name: str): - return None + return True + + def _current_kubectl_context(self) -> str: + return self._kubectx_applied def _get_kubectx_list(self) -> list[str]: return list(self._kubectx_values) @@ -149,3 +154,80 @@ def test_cluster_environment_layout_large_mock_data(env: str): assert app._k3s_service_status_console.text.cget("state") == "disabled" finally: root.destroy() + + +def test_kubectx_selection_requires_apply(): + root = tk.Tk() + root.withdraw() + try: + canvas = tk.Canvas(root, width=975, height=780, bg="white") + canvas.pack(fill="both", expand=False) + root.update() + + class _KubectxApplyApp(ScreenBaseMixin, ClusterScreenMixin): + def __init__(self, root: tk.Tk, canvas: tk.Canvas): + self.root = root + self.bg_canvas = canvas + self._canvas_items: list[int] = [] + self._overlay_widgets: list[tk.Widget] = [] + + self.cluster_env = tk.StringVar(master=root, value="prod") + self.selected_k3d_cluster = tk.StringVar(master=root, value="") + self.selected_kubectx = tk.StringVar(master=root, value="ctx-000") + self.service_namespace = tk.StringVar(master=root, value="knoe-system") + self.prod_artifacts_path = tk.StringVar(master=root, value="/tmp") + self.k3s_server_url = tk.StringVar(master=root, value="") + self.k3s_token = tk.StringVar(master=root, value="") + + self._kubectx_applied = "ctx-000" + self._switch_calls: list[str] = [] + + def _on_cluster_env_change(self, *_args): + return None + + def _get_k3d_cluster_list(self) -> list[str]: + return [] + + def _on_create_k3d_cluster(self): + return None + + def _on_delete_k3d_cluster(self): + return None + + def _on_k3d_cluster_select(self, *_args): + return None + + def _get_kubectx_list(self) -> list[str]: + return ["ctx-000", "ctx-001"] + + def _switch_kubectx(self, context_name: str) -> bool: + self._switch_calls.append(context_name) + return True + + def _verify_k3s_services(self): + return None + + def show_page(self, _page: str): + return None + + def _validate_and_save_cluster_config(self): + return True + + app = _KubectxApplyApp(root, canvas) + app._render_init_cluster_page() + root.update() + + # No pending change initially. + assert app._kubectx_apply_btn.cget("state") == "disabled" + + # Selecting a different context should NOT switch immediately. + app.selected_kubectx.set("ctx-001") + app._on_kubectx_select() + assert app._switch_calls == [] + assert app._kubectx_apply_btn.cget("state") == "normal" + + # Apply triggers the switch. + app._on_kubectx_apply() + assert app._switch_calls == ["ctx-001"] + finally: + root.destroy() diff --git a/tests/installer/test_verify_k3s_services_status.py b/tests/installer/test_verify_k3s_services_status.py new file mode 100644 index 0000000..d71c49a --- /dev/null +++ b/tests/installer/test_verify_k3s_services_status.py @@ -0,0 +1,39 @@ +from pathlib import Path +import sys +import subprocess + + +# Ensure `installer` is importable when tests are invoked directly via `pytest` +# (the project also provides `tests/run_tests.sh` which sets `PYTHONPATH=.`). +PROJECT_ROOT = Path(__file__).resolve().parents[2] +if str(PROJECT_ROOT) not in sys.path: + sys.path.insert(0, str(PROJECT_ROOT)) + + +from installer.core import env as core_env + + +def test_verify_k3s_services_status_does_not_require_token_when_kubeconfig_available( + monkeypatch, tmp_path +): + kubeconfig = tmp_path / "kubeconfig" + kubeconfig.write_text("dummy") + + def fake_run(cmd, capture_output=True, text=True, timeout=None, env=None): + return subprocess.CompletedProcess( + args=cmd, + returncode=0, + stdout="registry\nopenbao\nopentofu\n", + stderr="", + ) + + monkeypatch.setattr(core_env.subprocess, "run", fake_run) + + status = core_env._verify_k3s_services_status( + project_root=PROJECT_ROOT, + managed_kubeconfig=str(kubeconfig), + ) + + assert status["registry"] == "Good" + assert status["openbao"] == "Good" + assert status["opentofu"] == "Good" diff --git a/tests/test_no_common_services_literal_in_etc_scripts.py b/tests/test_no_common_services_literal_in_etc_scripts.py new file mode 100644 index 0000000..7193526 --- /dev/null +++ b/tests/test_no_common_services_literal_in_etc_scripts.py @@ -0,0 +1,13 @@ +from __future__ import annotations + +from pathlib import Path + + +def test_no_common_services_literal_in_etc_init_scripts(): + """Regression: registry namespace must be driven by config; do not hardcode `common-services`.""" + + init_scripts = sorted(Path("etc").glob("init_*.sh")) + assert init_scripts, "expected init_*.sh scripts under etc/" + for p in init_scripts: + text = p.read_text(encoding="utf-8") + assert "common-services" not in text, f"{p} hardcodes common-services" \ No newline at end of file