mirror of
https://github.com/dredx/prole.git
synced 2026-09-23 10:13:58 +00:00
Summary: Removed the prole-db-manager microservice and simplified deployment to use prole-authority as the internal management and authorization point. Fixed two blocking bugs that prevented silent install from completing on knoe-dev-cluster. Removed: prole-db-manager - Deleted db-manager-deployment.yaml and db-manager-service.yaml from opentofu manifests - Deleted src/db-manager/ (Dockerfile, server.js, package.json, tests) - Removed prole-db-manager port-forward mapping from installer/core/env.py - Removed init_db_manager.sh from Initialization Scripts (milestones.py, actions.py) - Removed init_certmgr.sh and init_db_manager.sh tabs from services screen (services.py) - Removed live k8s Deployment/Service from knoe-dev-cluster Fixed: PostgreSQL version downgrade error (pg17 -> pg18) - Created conf/postgresql/.version with value 18 - Updated k8s/prole/prole-db.yaml and prole-db-recovery.yaml.tpl imageName to prole-db:18-089 - Fixed _init_database_options_state() to restore saved version_type from prole.cfg so db_version_type defaults to v18 (pg18) instead of silently reverting to pg17 - Added database_options.* keys to _collect_input_snapshot() in cfg.py so distribution, version_type, and all extension toggles persist to prole.cfg Fixed: Cluster name inconsistency - Removed stale prole-dev-cluster references; all scripts now use knoe-dev-cluster - Added knoe-dev-cluster to mode-detection case in etc/prole_cfg.sh Config: conf/prole.cfg - Set kerberos_config.enabled = False, KERBEROS_AUTO_ENABLED = False - Added database_options.distribution = percona, version_type = v18 - Added all 13 extension flags set to True (postgis, pgvector, pgcrypto, pgaudit, pg_repack, pg_stat_statements, pg_buffercache, pg_freespacemap, pgrowlocks, postgres_fdw, dblink, pg_stat_monitor, pgbadger) Verification: ./install.py -s -l -v -c conf/prole.cfg completed successfully. CNPG deployed prole-db:18-089 to knoe-dev-cluster; all milestones passed. Co-authored-by: Junie <junie@jetbrains.com>
321 lines
11 KiB
Python
321 lines
11 KiB
Python
"""
|
|
Unit tests for installer/core/monitor.py
|
|
|
|
Covers the pure-logic helpers:
|
|
_parse_pf_cfg, write_port_forwards_cfg, check_port_conflicts, _build_kubectl_cmd,
|
|
_start_port_forwards
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import subprocess
|
|
from pathlib import Path
|
|
from unittest.mock import MagicMock, patch, call
|
|
import pytest
|
|
|
|
from installer.core.monitor import (
|
|
_parse_pf_cfg,
|
|
write_port_forwards_cfg,
|
|
check_port_conflicts,
|
|
_build_kubectl_cmd,
|
|
_start_port_forwards,
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# _parse_pf_cfg
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def test_parse_pf_cfg_missing_file(tmp_path):
|
|
result = _parse_pf_cfg(tmp_path / "nonexistent.cfg")
|
|
assert result == []
|
|
|
|
|
|
def test_parse_pf_cfg_empty_file(tmp_path):
|
|
cfg = tmp_path / "pf.cfg"
|
|
cfg.write_text("")
|
|
assert _parse_pf_cfg(cfg) == []
|
|
|
|
|
|
def test_parse_pf_cfg_comments_and_blanks(tmp_path):
|
|
cfg = tmp_path / "pf.cfg"
|
|
cfg.write_text("# this is a comment\n\n \n")
|
|
assert _parse_pf_cfg(cfg) == []
|
|
|
|
|
|
def test_parse_pf_cfg_no_colon(tmp_path):
|
|
cfg = tmp_path / "pf.cfg"
|
|
cfg.write_text("some line without colon\n")
|
|
assert _parse_pf_cfg(cfg) == []
|
|
|
|
|
|
def test_parse_pf_cfg_single_entry(tmp_path):
|
|
cfg = tmp_path / "pf.cfg"
|
|
cfg.write_text("argocd: local=8080 remote=80 ns=argocd svc=argocd-server address=127.0.0.1\n")
|
|
result = _parse_pf_cfg(cfg)
|
|
assert len(result) == 1
|
|
m = result[0]
|
|
assert m["id"] == "argocd"
|
|
assert m["local"] == "8080"
|
|
assert m["remote"] == "80"
|
|
assert m["ns"] == "argocd"
|
|
assert m["svc"] == "argocd-server"
|
|
assert m["address"] == "127.0.0.1"
|
|
|
|
|
|
def test_parse_pf_cfg_multiple_entries(tmp_path):
|
|
cfg = tmp_path / "pf.cfg"
|
|
cfg.write_text(
|
|
"# Port forwards\n"
|
|
"argocd: local=8080 remote=80 ns=argocd svc=argocd-server address=127.0.0.1\n"
|
|
"openbao: local=8200 remote=8200 ns=default svc=openbao address=127.0.0.1\n"
|
|
)
|
|
result = _parse_pf_cfg(cfg)
|
|
assert len(result) == 2
|
|
assert result[0]["id"] == "argocd"
|
|
assert result[1]["id"] == "openbao"
|
|
assert result[1]["local"] == "8200"
|
|
|
|
|
|
def test_parse_pf_cfg_token_without_equals(tmp_path):
|
|
"""Tokens without '=' should be silently ignored."""
|
|
cfg = tmp_path / "pf.cfg"
|
|
cfg.write_text("myid: local=9000 badtoken remote=80\n")
|
|
result = _parse_pf_cfg(cfg)
|
|
assert len(result) == 1
|
|
assert result[0]["local"] == "9000"
|
|
assert result[0]["remote"] == "80"
|
|
# 'badtoken' has no '=' so it's not added
|
|
assert "badtoken" not in result[0]
|
|
|
|
|
|
def test_parse_pf_cfg_id_with_extra_whitespace(tmp_path):
|
|
cfg = tmp_path / "pf.cfg"
|
|
cfg.write_text(" myservice : local=1234 remote=5678\n")
|
|
result = _parse_pf_cfg(cfg)
|
|
assert len(result) == 1
|
|
assert result[0]["id"] == "myservice"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# write_port_forwards_cfg
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def test_write_port_forwards_cfg_empty(tmp_path):
|
|
out = tmp_path / "out.cfg"
|
|
write_port_forwards_cfg(out, [])
|
|
content = out.read_text()
|
|
assert "# Port forward configuration" in content
|
|
assert "# Format:" in content
|
|
|
|
|
|
def test_write_port_forwards_cfg_skips_no_id(tmp_path):
|
|
out = tmp_path / "out.cfg"
|
|
# Mapping string with no id= token
|
|
write_port_forwards_cfg(out, ["hostPort=8080;servicePort=80"])
|
|
content = out.read_text()
|
|
# No entry line should be written (id is empty)
|
|
lines = [l for l in content.splitlines() if l and not l.startswith("#")]
|
|
assert lines == []
|
|
|
|
|
|
def test_write_port_forwards_cfg_single_mapping(tmp_path):
|
|
out = tmp_path / "out.cfg"
|
|
mapping = "id=argocd;namespace=argocd;target=svc/argocd-server;hostPort=8080;servicePort=80;address=127.0.0.1"
|
|
write_port_forwards_cfg(out, [mapping])
|
|
content = out.read_text()
|
|
assert "argocd: local=8080 remote=80 ns=argocd svc=argocd-server address=127.0.0.1" in content
|
|
|
|
|
|
def test_write_port_forwards_cfg_target_without_svc_prefix(tmp_path):
|
|
out = tmp_path / "out.cfg"
|
|
mapping = "id=myapp;namespace=default;target=myapp-service;hostPort=9000;servicePort=9000;address=0.0.0.0"
|
|
write_port_forwards_cfg(out, [mapping])
|
|
content = out.read_text()
|
|
# target doesn't start with svc/, so svc = target as-is
|
|
assert "svc=myapp-service" in content
|
|
|
|
|
|
def test_write_port_forwards_cfg_roundtrip(tmp_path):
|
|
"""Written file can be parsed back by _parse_pf_cfg."""
|
|
out = tmp_path / "pf.cfg"
|
|
mappings = [
|
|
"id=argocd;namespace=argocd;target=svc/argocd-server;hostPort=8080;servicePort=80;address=127.0.0.1",
|
|
"id=openbao;namespace=default;target=svc/openbao;hostPort=8200;servicePort=8200;address=127.0.0.1",
|
|
]
|
|
write_port_forwards_cfg(out, mappings)
|
|
parsed = _parse_pf_cfg(out)
|
|
assert len(parsed) == 2
|
|
assert parsed[0]["id"] == "argocd"
|
|
assert parsed[1]["id"] == "openbao"
|
|
assert parsed[0]["local"] == "8080"
|
|
assert parsed[1]["remote"] == "8200"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# check_port_conflicts
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def test_check_port_conflicts_empty():
|
|
assert check_port_conflicts([]) == []
|
|
|
|
|
|
def test_check_port_conflicts_no_conflicts():
|
|
mappings = [
|
|
{"id": "a", "local": "8080"},
|
|
{"id": "b", "local": "8200"},
|
|
{"id": "c", "local": "9000"},
|
|
]
|
|
assert check_port_conflicts(mappings) == []
|
|
|
|
|
|
def test_check_port_conflicts_single_conflict():
|
|
mappings = [
|
|
{"id": "a", "local": "8080"},
|
|
{"id": "b", "local": "8080"},
|
|
]
|
|
result = check_port_conflicts(mappings)
|
|
assert len(result) == 1
|
|
assert "8080" in result[0]
|
|
assert "a" in result[0]
|
|
assert "b" in result[0]
|
|
|
|
|
|
def test_check_port_conflicts_multiple_conflicts():
|
|
mappings = [
|
|
{"id": "a", "local": "8080"},
|
|
{"id": "b", "local": "8080"},
|
|
{"id": "c", "local": "9000"},
|
|
{"id": "d", "local": "9000"},
|
|
]
|
|
result = check_port_conflicts(mappings)
|
|
assert len(result) == 2
|
|
|
|
|
|
def test_check_port_conflicts_missing_local_key():
|
|
"""Mappings without 'local' key use '' as port and should not crash."""
|
|
mappings = [
|
|
{"id": "a"},
|
|
{"id": "b"},
|
|
]
|
|
result = check_port_conflicts(mappings)
|
|
# Both have empty port '' — should detect conflict
|
|
assert len(result) == 1
|
|
|
|
|
|
def test_check_port_conflicts_missing_id_key():
|
|
"""Mappings without 'id' key should not crash."""
|
|
mappings = [
|
|
{"local": "8080"},
|
|
{"local": "8080"},
|
|
]
|
|
result = check_port_conflicts(mappings)
|
|
assert len(result) == 1
|
|
assert "?" in result[0]
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# _build_kubectl_cmd
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def test_build_kubectl_cmd_basic():
|
|
m = {"ns": "argocd", "svc": "argocd-server", "local": "8080", "remote": "80", "address": "127.0.0.1"}
|
|
cmd = _build_kubectl_cmd(m)
|
|
assert cmd == ["kubectl", "port-forward", "-n", "argocd", "svc/argocd-server", "8080:80"]
|
|
# address is 127.0.0.1 so --address is NOT added
|
|
assert "--address" not in cmd
|
|
|
|
|
|
def test_build_kubectl_cmd_non_localhost_address():
|
|
m = {"ns": "default", "svc": "myapp", "local": "9000", "remote": "9000", "address": "0.0.0.0"}
|
|
cmd = _build_kubectl_cmd(m)
|
|
assert "--address" in cmd
|
|
assert "0.0.0.0" in cmd
|
|
|
|
|
|
def test_build_kubectl_cmd_svc_prefix_not_doubled():
|
|
"""svc/ prefix should only appear once."""
|
|
m = {"ns": "default", "svc": "svc/myapp", "local": "9000", "remote": "9000", "address": "127.0.0.1"}
|
|
cmd = _build_kubectl_cmd(m)
|
|
svc_arg = cmd[4]
|
|
assert svc_arg == "svc/myapp"
|
|
assert not svc_arg.startswith("svc/svc/")
|
|
|
|
|
|
def test_build_kubectl_cmd_adds_svc_prefix():
|
|
"""Service name without svc/ prefix should get one."""
|
|
m = {"ns": "default", "svc": "myapp", "local": "9000", "remote": "9000", "address": "127.0.0.1"}
|
|
cmd = _build_kubectl_cmd(m)
|
|
assert "svc/myapp" in cmd
|
|
|
|
|
|
def test_build_kubectl_cmd_defaults():
|
|
"""Missing keys should fall back to defaults."""
|
|
cmd = _build_kubectl_cmd({})
|
|
assert cmd[0] == "kubectl"
|
|
assert "-n" in cmd
|
|
assert "default" in cmd # default namespace
|
|
|
|
|
|
def test_build_kubectl_cmd_empty_address():
|
|
"""Empty address string should not add --address flag."""
|
|
m = {"ns": "default", "svc": "myapp", "local": "9000", "remote": "9000", "address": ""}
|
|
cmd = _build_kubectl_cmd(m)
|
|
assert "--address" not in cmd
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# _start_port_forwards
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def test_start_port_forwards_empty():
|
|
result = _start_port_forwards([])
|
|
assert result == []
|
|
|
|
|
|
def test_start_port_forwards_success(capsys):
|
|
mapping = {"id": "argocd", "ns": "argocd", "svc": "argocd-server", "local": "8080", "remote": "80", "address": "127.0.0.1"}
|
|
mock_proc = MagicMock(spec=subprocess.Popen)
|
|
with patch("installer.core.monitor.subprocess.Popen", return_value=mock_proc) as mock_popen:
|
|
result = _start_port_forwards([mapping])
|
|
assert len(result) == 1
|
|
m_out, proc_out = result[0]
|
|
assert m_out is mapping
|
|
assert proc_out is mock_proc
|
|
out = capsys.readouterr().out
|
|
assert "argocd" in out
|
|
assert "✓" in out
|
|
|
|
|
|
def test_start_port_forwards_failure(capsys):
|
|
mapping = {"id": "broken", "ns": "default", "svc": "nosvc", "local": "9999", "remote": "9999", "address": "127.0.0.1"}
|
|
with patch("installer.core.monitor.subprocess.Popen", side_effect=OSError("not found")):
|
|
result = _start_port_forwards([mapping])
|
|
assert len(result) == 1
|
|
_, proc = result[0]
|
|
assert proc is None
|
|
out = capsys.readouterr().out
|
|
assert "✗" in out
|
|
assert "broken" in out
|
|
|
|
|
|
def test_start_port_forwards_verbose(capsys):
|
|
mapping = {"id": "test", "ns": "default", "svc": "myapp", "local": "8000", "remote": "8000", "address": "127.0.0.1"}
|
|
mock_proc = MagicMock(spec=subprocess.Popen)
|
|
with patch("installer.core.monitor.subprocess.Popen", return_value=mock_proc):
|
|
_start_port_forwards([mapping], verbose=True)
|
|
out = capsys.readouterr().out
|
|
assert "[PORT-FWD]" in out
|
|
|
|
|
|
def test_start_port_forwards_multiple(capsys):
|
|
mappings = [
|
|
{"id": "a", "ns": "default", "svc": "svc-a", "local": "8001", "remote": "80", "address": "127.0.0.1"},
|
|
{"id": "b", "ns": "default", "svc": "svc-b", "local": "8002", "remote": "80", "address": "127.0.0.1"},
|
|
]
|
|
mock_proc = MagicMock(spec=subprocess.Popen)
|
|
with patch("installer.core.monitor.subprocess.Popen", return_value=mock_proc):
|
|
result = _start_port_forwards(mappings)
|
|
assert len(result) == 2
|
|
assert result[0][0]["id"] == "a"
|
|
assert result[1][0]["id"] == "b"
|