prole/tests/installer/test_cluster_nodes_render_smoke.py
chrisfu 3312c39b1f feat: GCP/GKE CNPG hardening, Artifact Registry traffic light, and knoe-system namespace fixes
UI screens
- database.py: fix mode detection to use env_key priority (prod→k8s, service→k3s) so stale DEPLOYMENT_MODE never overrides the user's chosen environment
- database.py: Registry status reads ARTIFACT_REGISTRY_AVAILABLE persisted by cluster screen; uses SERVICE_NAMESPACE for Artifact Registry repo name
- cluster.py: add Artifact Registry traffic light (amber→green/red) to prod section; _check_artifact_registry_async persists ARTIFACT_REGISTRY_AVAILABLE into Global cfg
- cluster.py: re-trigger Artifact Registry check after GKE cluster selection so the light re-evaluates once region is available from KUBECONTEXT
- cluster_nodes.py: fix TclError on Python 3.14 — pady=(2,0) tuple → pady=2 scalar
- __init__.py: seed knoe-system namespace when saved value is "default", not only when empty
- services.py: replace hardcoded "Prole DB" log string with dynamic cnpg_cluster name

Core ops
- cloudnative_pg.py: replace one-shot Barman plugin retry with 6-attempt loop; first cert-manager/x509 failure triggers rollout restart + 30 s CA propagation wait; subsequent failures back off up to 60 s per attempt
- cloudnative_pg.py: TLS CA CN now uses cluster_name instead of hardcoded "Prole CNPG CA"
- registry.py, garage_store.py: refactored into per-mode modules (k3d/k3s/k8s registry and garage store, shared _garage_common)

Deploy / config
- deploy/gcp/gke/knoe-db.yaml: GKE-specific CNPG cluster manifest (rw/ro/r on separate nodes with premium-rwo storage)
- etc/init_common_services.sh, modes/k8s/knoe-db/.version: updated for current deploy
- kong-deployment.yaml: updated manifest

Tests
- test_cluster_nodes_render_smoke.py: add pack/grid, winfo_children, winfo_reqheight, update_idletasks, grid_slaves to dummy widgets; monkeypatch tk.Label so CNPG placement render completes without a real Tkinter root

Co-authored-by: Junie <junie@jetbrains.com>
2026-04-04 19:36:08 -07:00

149 lines
4.3 KiB
Python

from __future__ import annotations
import knoe.ui.screens.cluster_nodes as cluster_nodes
from knoe.ui.screens.cluster_nodes import ClusterNodesScreenMixin
def test_render_cluster_nodes_page_topology_messages_are_scrollable(monkeypatch):
class _DummyWidget:
def __init__(self, *_args, **_kwargs):
pass
def destroy(self):
return None
def place_forget(self):
return None
def configure(self, **_kwargs):
return None
def pack(self, *_args, **_kwargs):
return None
def grid(self, *_args, **_kwargs):
return None
class _DummyFrame(_DummyWidget):
def winfo_children(self):
return [_DummyWidget()]
def winfo_reqheight(self):
return 0
def update_idletasks(self):
return None
def grid_slaves(self, *_args, **_kwargs):
return [_DummyWidget()]
class _DummyScrollbar(_DummyWidget):
instances: list["_DummyScrollbar"] = []
def __init__(self, *_args, **kwargs):
super().__init__()
self.command = kwargs.get("command")
_DummyScrollbar.instances.append(self)
def pack(self, *_args, **_kwargs):
return None
def set(self, *_args, **_kwargs):
return None
def configure(self, **kwargs):
if "command" in kwargs:
self.command = kwargs["command"]
class _DummyText(_DummyWidget):
instances: list["_DummyText"] = []
def __init__(self, *_args, **kwargs):
super().__init__()
self.yscrollcommand = kwargs.get("yscrollcommand")
self.contents = ""
self.state = "normal"
_DummyText.instances.append(self)
def pack(self, *_args, **_kwargs):
return None
def insert(self, _idx: str, text: str):
self.contents += text
def see(self, *_args, **_kwargs):
return None
def configure(self, **kwargs):
if "state" in kwargs:
self.state = kwargs["state"]
def yview(self, *_args, **_kwargs):
return None
monkeypatch.setattr(cluster_nodes.tk, "Frame", _DummyFrame)
monkeypatch.setattr(cluster_nodes.tk, "Scrollbar", _DummyScrollbar)
monkeypatch.setattr(cluster_nodes.tk, "Text", _DummyText)
monkeypatch.setattr(cluster_nodes.tk, "Button", _DummyWidget)
monkeypatch.setattr(cluster_nodes.tk, "Label", _DummyWidget)
class _DummyCanvas:
def __init__(self):
self._next_id = 1
def create_text(self, *_args, **_kwargs):
item = self._next_id
self._next_id += 1
return item
def create_window(self, *_args, **_kwargs):
item = self._next_id
self._next_id += 1
return item
class _DummyApp(ClusterNodesScreenMixin):
def __init__(self):
self.bg_canvas = _DummyCanvas()
self._canvas_items: list[int] = []
self._overlay_widgets: list[object] = []
self._cluster_nodes_topology_state = {
"phase": "complete",
"messages": [f"line-{i}" for i in range(1, 7)],
"topology": None,
"topology_path": "",
"selected": set(),
"busy": True,
"expanded": None,
}
def _clear_canvas_page(self):
return None
def _should_show_cluster_nodes_screen(self):
return True
def _render_title(self, *_args, **_kwargs):
return None
def _render_paragraph(self, *_args, **_kwargs):
return None
def _cluster_nodes_start_discovery(self, force: bool = False):
return None
def _cluster_nodes_render_summary_cards(self, _topology, _x, y):
return y
def _cluster_nodes_render_table(self, _topology, _x, y):
return y
app = _DummyApp()
app._render_cluster_nodes_page()
assert _DummyScrollbar.instances
assert _DummyText.instances
assert _DummyText.instances[0].state == "disabled"
assert "line-1" in _DummyText.instances[0].contents
assert "line-6" in _DummyText.instances[0].contents
assert _DummyScrollbar.instances[0].command is not None