mirror of
https://github.com/dredx/prole.git
synced 2026-09-23 10:13:58 +00:00
Fix installer ctx apply flow and registry namespace
- Add explicit Apply button for kubectl context switching to avoid half-applied changes - Allow kubeconfig/context-based auth without requiring K3S_TOKEN when kubeconfig is valid - Prompt cleanup/reset of previous Common Core services namespace to prevent resource collisions - Remove hardcoded 'common-services' registry namespace; default registry deploy/check to SERVICE_NAMESPACE/REGISTRY_NAMESPACE - Update mocks and add regression tests for namespace resolution and installer behavior
This commit is contained in:
parent
436c6214df
commit
d397b3000e
@ -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.
|
||||
|
||||
@ -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"
|
||||
|
||||
|
||||
@ -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)"
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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() {
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 {}
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -215,14 +215,20 @@ class ClusterScreenMixin:
|
||||
)
|
||||
combo.bind("<<ComboboxSelected>>", 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:
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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"
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
155
tests/etc/test_init_kdc_k3s_registry_default.sh
Normal file
155
tests/etc/test_init_kdc_k3s_registry_default.sh
Normal file
@ -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 <<EOF > "$WORK_DIR/env.sh"
|
||||
#!/usr/bin/env bash
|
||||
export PROLE_HOME="$WORK_DIR"
|
||||
export PROLE_CONF="$WORK_DIR/conf"
|
||||
EOF
|
||||
|
||||
cat <<EOF > "$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 <<YAML
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: dummy
|
||||
YAML
|
||||
exit 0
|
||||
fi
|
||||
if [[ "${2:-}" == "namespace" ]]; then
|
||||
exit 0
|
||||
fi
|
||||
exit 0
|
||||
;;
|
||||
apply)
|
||||
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"
|
||||
@ -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"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@ -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()
|
||||
|
||||
39
tests/installer/test_verify_k3s_services_status.py
Normal file
39
tests/installer/test_verify_k3s_services_status.py
Normal file
@ -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"
|
||||
13
tests/test_no_common_services_literal_in_etc_scripts.py
Normal file
13
tests/test_no_common_services_literal_in_etc_scripts.py
Normal file
@ -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"
|
||||
Loading…
Reference in New Issue
Block a user