prole/tests/installer/test_core_classes.py
chrisfu 46f31843c2 Fix Kerberos test flow and improve multi-namespace support in k3s
- Resolve namespace drift in Kerberos test flow and OpenBao deployment.
- Fix OpenAPI credential-validation failures in etc scripts by using --validate=false with kubectl.
- Introduce preferred_service_namespace() to accurately target namespaces for KDC policy/test paths.
- Default Kerberos test user to 'administrator' and prefill from Ansible vault secrets.
- Generalized OpenBao deployment logic to correctly handle multi-namespace environments.
- Added regression test 'tests/etc/test_init_kerberos_k3s_real_kdc.sh'.
- Synchronized SERVICE_NAMESPACE extraction in prole_cfg.sh.

Co-authored-by: Junie <junie@jetbrains.com>
2026-03-01 00:57:10 -08:00

453 lines
15 KiB
Python

"""Tests for installer core classes: state, milestone, controller, build."""
from __future__ import annotations
import os
from pathlib import Path
from unittest import mock
import pytest
from installer.state import InstallerState
from installer.milestone import Milestone, ProgressCallback
from installer.build import get_build_command, BuildMilestone
from installer.core.controller import ProleController
from installer.core.actions import ProleSilentInstaller
# ===== InstallerState =====
class TestInstallerState:
def test_defaults(self):
s = InstallerState()
assert s.current_id is None
assert s.data == {}
assert s.inputs == {}
assert s.config_data == {}
assert s.errors == []
assert s.progress == {}
assert s.history == []
assert s.completed == set()
assert s.controller is None
def test_mark_completed(self):
s = InstallerState()
s.mark_completed("step1")
s.mark_completed("step2")
assert "step1" in s.completed
assert "step2" in s.completed
def test_mark_completed_idempotent(self):
s = InstallerState()
s.mark_completed("step1")
s.mark_completed("step1")
assert len(s.completed) == 1
def test_with_controller(self):
s = InstallerState(controller="fake")
assert s.controller == "fake"
def test_inputs_and_config(self):
s = InstallerState(
inputs={"key": "val"},
config_data={"section": {"k": "v"}},
)
assert s.inputs["key"] == "val"
assert s.config_data["section"]["k"] == "v"
# ===== Milestone base class =====
class _DummyMilestone(Milestone):
"""Concrete subclass for testing the abstract base."""
def execute(self, state, progress=None):
if progress:
progress("running", 0.5)
class TestMilestoneBase:
def test_init(self):
m = _DummyMilestone("test_id", "Test Title")
assert m.id == "test_id"
assert m.title == "Test Title"
def test_validate_default(self):
m = _DummyMilestone("x", "X")
assert m.validate(InstallerState()) is None
def test_next_default(self):
m = _DummyMilestone("x", "X")
assert m.next(InstallerState()) is None
def test_execute_with_progress(self):
m = _DummyMilestone("x", "X")
msgs = []
m.execute(InstallerState(), progress=lambda msg, p: msgs.append((msg, p)))
assert ("running", 0.5) in msgs
def test_parse_bool(self):
m = _DummyMilestone("x", "X")
assert m._parse_bool("true") is True
assert m._parse_bool("false") is False
assert m._parse_bool(None) is False
assert m._parse_bool(None, default=True) is True
def test_run_cmd_list(self):
m = _DummyMilestone("x", "X")
rc = m._run_cmd(["echo", "hello"])
assert rc == 0
def test_run_cmd_string(self):
m = _DummyMilestone("x", "X")
rc = m._run_cmd("echo hello")
assert rc == 0
def test_run_cmd_with_stdout(self):
m = _DummyMilestone("x", "X")
lines = []
rc = m._run_cmd(
["echo", "test_output"], on_stdout=lambda l: lines.append(l.strip())
)
assert rc == 0
assert "test_output" in lines
def test_run_cmd_failure(self):
m = _DummyMilestone("x", "X")
rc = m._run_cmd(["false"])
assert rc != 0
def test_run_cmd_bad_command(self):
m = _DummyMilestone("x", "X")
rc = m._run_cmd(["/nonexistent/command"])
assert rc == 1
def test_get_script_env_basic(self):
m = _DummyMilestone("x", "X")
state = InstallerState(
inputs={
"init_password.db_namespace": "myns",
"init_password.db_username": "testuser",
"init_password.db_password": "secret123",
"init_cluster.cluster_env": "dev",
"kerberos_config.realm": "EXAMPLE.COM",
"kerberos_config.kdc": "10.0.0.1",
"kerberos_config.user": "admin",
"kerberos_config.password": "krbpass",
"kerberos_config.enabled": "True",
"init_cluster.at_rest_encryption_enabled": "true",
},
config_data={},
)
env = m._get_script_env(state)
assert env["NAMESPACE"] == "myns"
assert env["DB_PASSWORD"] == "secret123"
assert env["PROLE_DB_USER"] == "testuser"
assert env["PROLE_MODE"] == "k3d"
assert env["KRB5_REALM"] == "EXAMPLE.COM"
assert env["KERBEROS_ENABLED"] == "True"
assert env["AT_REST_ENCRYPTION_ENABLED"] == "true"
assert env["REALM"] == "EXAMPLE.COM"
assert env["DOMAIN"] == "example.com"
def test_get_script_env_defaults(self):
m = _DummyMilestone("x", "X")
state = InstallerState(inputs={}, config_data={})
env = m._get_script_env(state)
assert env["NAMESPACE"] == "default"
assert env["PROLE_MODE"] == "k3d"
assert env["KERBEROS_ENABLED"] == "False"
def test_get_script_env_grafana_from_config(self):
m = _DummyMilestone("x", "X")
state = InstallerState(
inputs={"init_password.db_password": "dbpw"},
config_data={"Monitoring": {"GRAFANA_ADMIN_PASSWORD": "grafanapw"}},
)
env = m._get_script_env(state)
assert env["GRAFANA_ADMIN_PASSWORD"] == "grafanapw"
def test_get_script_env_grafana_fallback(self):
m = _DummyMilestone("x", "X")
state = InstallerState(
inputs={"init_password.db_password": "dbpw"},
config_data={},
)
env = m._get_script_env(state)
assert env["GRAFANA_ADMIN_PASSWORD"] == "dbpw"
def test_get_script_env_k3s_mode(self):
m = _DummyMilestone("x", "X")
state = InstallerState(
inputs={
"init_cluster.cluster_env": "service",
"init_cluster.k3s_server_url": "myserver:6443",
"init_cluster.k3s_token": "mytoken",
},
config_data={},
)
env = m._get_script_env(state)
assert env["PROLE_MODE"] == "k3s"
assert env["PROLE_K3S_SERVER"] == "https://myserver:6443"
assert env["PROLE_K3S_TOKEN"] == "mytoken"
def test_get_script_env_argocd_from_config(self):
m = _DummyMilestone("x", "X")
state = InstallerState(
inputs={},
config_data={
"Global": {
"ARGOCD_NAMESPACE": "custom-argocd",
"REGISTRY_NAMESPACE": "custom-reg",
}
},
)
env = m._get_script_env(state)
assert env["ARGOCD_NAMESPACE"] == "custom-argocd"
assert env["REGISTRY_NAMESPACE"] == "custom-reg"
# ===== build.py =====
class TestBuildHelpers:
def test_get_build_command(self, tmp_path):
cmd = get_build_command(tmp_path, "dev")
assert "cd" in cmd
assert str(tmp_path) in cmd
assert "# Dev" in cmd
def test_get_build_command_service(self, tmp_path):
cmd = get_build_command(tmp_path, "service")
assert "# Service" in cmd
def test_get_build_command_none_env(self, tmp_path):
cmd = get_build_command(tmp_path, None)
assert "# Dev" in cmd
class TestBuildMilestone:
def test_init(self, tmp_path):
m = BuildMilestone(tmp_path, "dev", "next_step")
assert m.id == "build"
assert m.title == "Build"
assert m._next_id == "next_step"
def test_validate_exists(self, tmp_path):
m = BuildMilestone(tmp_path)
assert m.validate(InstallerState()) is None
def test_validate_missing(self):
m = BuildMilestone(Path("/nonexistent/path"))
result = m.validate(InstallerState())
assert result is not None
assert len(result) > 0
def test_execute(self, tmp_path):
m = BuildMilestone(tmp_path, "dev")
state = InstallerState()
msgs = []
m.execute(state, progress=lambda msg, p: msgs.append(msg))
assert "build.command" in state.data
assert "Build command prepared" in msgs
def test_next(self, tmp_path):
m = BuildMilestone(tmp_path, next_id="deploy")
assert m.next(InstallerState()) == "deploy"
def test_next_none(self, tmp_path):
m = BuildMilestone(tmp_path)
assert m.next(InstallerState()) is None
# ===== ProleController =====
class TestProleController:
def test_init(self, tmp_path):
c = ProleController(tmp_path)
assert c.project_root == tmp_path
assert c.verbose is False
assert c.state is not None
installer = ProleSilentInstaller(c)
defaults = installer._default_inputs()
assert defaults.get("kerberos_config.user") == "administrator"
assert defaults.get("kerberos_config.password") == ""
def test_init_verbose(self, tmp_path):
c = ProleController(tmp_path, verbose=True)
assert c.verbose is True
def test_check_docker_running(self, tmp_path):
c = ProleController(tmp_path)
result = c.check_docker_running()
assert isinstance(result, bool)
def test_get_prole_db_version_defaults(self, tmp_path):
c = ProleController(tmp_path)
version = c.get_prole_db_version()
assert "17.7" in version # default pg version
assert "-" in version
def test_get_prole_db_version_custom(self, tmp_path):
pg_dir = tmp_path / "conf" / "postgresql"
pg_dir.mkdir(parents=True)
(pg_dir / ".version").write_text("16.2")
db_dir = tmp_path / "prole-db"
db_dir.mkdir()
(db_dir / ".version").write_text("42")
c = ProleController(tmp_path)
version = c.get_prole_db_version()
assert version == "16.2-042"
def test_run_milestones(self, tmp_path):
c = ProleController(tmp_path)
m1 = _DummyMilestone("step1", "Step 1")
m2 = _DummyMilestone("step2", "Step 2")
msgs = []
c.run_milestones([m1, m2], progress_callback=lambda msg, p: msgs.append(msg))
assert "step1" in c.state.completed
assert "step2" in c.state.completed
def test_run_milestones_no_callback(self, tmp_path):
c = ProleController(tmp_path)
m = _DummyMilestone("s1", "S1")
c.run_milestones([m])
assert "s1" in c.state.completed
def test_run_script_missing(self, tmp_path):
c = ProleController(tmp_path)
# Script doesn't exist but should not crash
rc = c.run_script("nonexistent_script.sh", args=[])
# Will fail since the script doesn't exist
assert rc != 0 or rc is None
# ===== Milestone concrete subclasses (from core/milestones.py) =====
class TestDependenciesMilestone:
def test_init(self):
from installer.core.milestones import DependenciesMilestone
m = DependenciesMilestone()
assert m.id == "dependencies"
assert m.title == "Dependency Verification"
class TestNetworkScanMilestone:
def test_init(self):
from installer.core.milestones import NetworkScanMilestone
m = NetworkScanMilestone()
assert m.id == "network_scan"
class TestEnvSetupMilestone:
def test_execute(self):
from installer.core.milestones import EnvSetupMilestone
m = EnvSetupMilestone()
state = InstallerState(
inputs={
"env_setup.PROLE_HOME": "/opt/prole",
"env_setup.PROLE_CONF": "/opt/prole/conf",
"env_setup.NAMESPACE": "testns",
},
config_data={},
)
msgs = []
m.execute(state, progress=lambda msg, p: msgs.append(msg))
assert state.config_data["System Environment"]["PROLE_HOME"] == "/opt/prole"
assert state.config_data["System Environment"]["NAMESPACE"] == "testns"
assert "Environment setup complete" in msgs
class TestSecretManagementMilestone:
def test_execute_plain(self):
from installer.core.milestones import SecretManagementMilestone
m = SecretManagementMilestone()
state = InstallerState(
inputs={
"init_password.db_password": "plaintext123",
},
)
msgs = []
m.execute(state, progress=lambda msg, p: msgs.append(msg))
assert state.inputs["init_password.db_password_confirm"] == "plaintext123"
def test_execute_no_passwords(self):
from installer.core.milestones import SecretManagementMilestone
m = SecretManagementMilestone()
state = InstallerState(inputs={})
m.execute(state)
class TestDatabaseCreationMilestone:
def test_execute_generates_password(self):
from installer.core.milestones import DatabaseCreationMilestone
m = DatabaseCreationMilestone()
state = InstallerState(
inputs={"env_setup.NAMESPACE": "testns"},
config_data={},
)
m.execute(state)
assert state.inputs.get("init_password.db_password")
assert len(state.inputs["init_password.db_password"]) >= 20
assert state.config_data["Database Creation"]["DB_NAME"] == "testns"
def test_execute_with_password(self):
from installer.core.milestones import DatabaseCreationMilestone
m = DatabaseCreationMilestone()
state = InstallerState(
inputs={
"init_password.db_password": "mypassword",
"env_setup.NAMESPACE": "ns1",
},
config_data={},
)
m.execute(state)
assert state.inputs["init_password.db_password"] == "mypassword"
def test_execute_unresolvable_openbao(self):
from installer.core.milestones import DatabaseCreationMilestone
m = DatabaseCreationMilestone()
state = InstallerState(
inputs={
"init_password.db_password": "${OPENBAO:kv/prole/ns/db#password}",
"env_setup.NAMESPACE": "ns1",
},
config_data={},
)
m.execute(state)
pw = state.inputs["init_password.db_password"]
assert not pw.startswith("${")
assert len(pw) >= 20
class TestDockerBuildMilestone:
def test_disabled(self):
from installer.core.milestones import DockerBuildMilestone
m = DockerBuildMilestone()
state = InstallerState(
inputs={"init_db_build.run_build": "False"},
config_data={},
)
m.execute(state) # Should not raise
class TestDeploymentMilestone:
def test_init(self):
from installer.core.milestones import DeploymentMilestone
m = DeploymentMilestone()
assert m.id == "deployment"