prole/tests/installer/test_actions_helpers.py
chrisfu 1573bb59c2 Add prole.spec and extend OpenTofu k3s configuration
- Include new `prole.spec` for build configurations and dependencies.
- Add Terraform state handling for OpenTofu in `k3s` cluster.
- Provision multiple Kubernetes resources in `prole-db` namespace: namespace, services, ConfigMaps, StatefulSets, Ingress rules, and PersistentVolumes.
- Integrate deployment and configuration enhancements for `garage`, `prole`, and related components.
2026-02-24 21:47:02 -08:00

304 lines
12 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Tests for installer/core/actions.py – ProleInstallerBase helper methods."""
from __future__ import annotations
import os
import sys
from pathlib import Path
from unittest import mock
from unittest.mock import MagicMock, patch
import pytest
from installer.core.actions import ProleInstallerBase, ProleSilentInstaller, _configure_unbuffered_io
# ---------------------------------------------------------------------------
# Concrete subclass for testing the abstract base
# ---------------------------------------------------------------------------
class _TestableInstaller(ProleInstallerBase):
"""Minimal concrete subclass for testing base-class helpers."""
def __init__(self, inputs=None, project_root=None):
self._inputs = inputs or {}
self.project_root = project_root or Path('/tmp/fake-prole')
self.controller = MagicMock()
self._init_shared_state()
def _get_input(self, key: str, default: str | None = None) -> str:
return self._inputs.get(key, default or '')
# ---------------------------------------------------------------------------
# _configure_unbuffered_io
# ---------------------------------------------------------------------------
class TestConfigureUnbufferedIO:
def test_sets_env_var(self):
with mock.patch.dict(os.environ, {}, clear=False):
os.environ.pop('PYTHONUNBUFFERED', None)
_configure_unbuffered_io()
assert os.environ.get('PYTHONUNBUFFERED') == '1'
def test_does_not_overwrite_existing(self):
with mock.patch.dict(os.environ, {'PYTHONUNBUFFERED': 'already'}, clear=False):
_configure_unbuffered_io()
assert os.environ['PYTHONUNBUFFERED'] == 'already'
# ---------------------------------------------------------------------------
# _init_shared_state
# ---------------------------------------------------------------------------
class TestInitSharedState:
def test_initial_sections(self):
inst = _TestableInstaller()
expected_sections = [
'Global', 'Welcome', 'Dependencies', 'Network',
'Port Forwards', 'System Environment', 'Kerberos Authentication',
'Ollama', 'Optional Features', 'Database Creation',
'Docker Build', 'Initialize Cluster', 'Initialization Scripts',
'Deployment', 'Dev Cluster (k3d)', 'Service Cluster (k3s)',
'Prod Cluster (k8s)', 'Install',
]
for section in expected_sections:
assert section in inst.prole_cfg_data
def test_initial_flags(self):
inst = _TestableInstaller()
assert inst._cfg_secret_cache == {}
assert inst._secrets_finalized is False
assert inst._managed_kubeconfig is None
assert inst._repair_ran is False
# ---------------------------------------------------------------------------
# _get_input_bool
# ---------------------------------------------------------------------------
class TestGetInputBool:
def test_true_values(self):
for val in ('true', '1', 'yes', 'on', 'True', 'YES'):
inst = _TestableInstaller(inputs={'key': val})
assert inst._get_input_bool('key') is True
def test_false_values(self):
for val in ('false', '0', 'no', 'off', 'False', 'NO'):
inst = _TestableInstaller(inputs={'key': val})
assert inst._get_input_bool('key') is False
def test_default(self):
inst = _TestableInstaller()
assert inst._get_input_bool('missing_key', default=True) is True
assert inst._get_input_bool('missing_key', default=False) is False
# ---------------------------------------------------------------------------
# Logging
# ---------------------------------------------------------------------------
class TestLogging:
def test_log_prints(self, capsys):
inst = _TestableInstaller()
inst.log("hello world")
captured = capsys.readouterr()
assert "hello world" in captured.out
def test_err_prints_to_stderr(self, capsys):
inst = _TestableInstaller()
inst.err("error msg")
captured = capsys.readouterr()
assert "error msg" in captured.err
# ---------------------------------------------------------------------------
# _deployment_mode
# ---------------------------------------------------------------------------
class TestDeploymentMode:
def test_dev_mode(self):
inst = _TestableInstaller(inputs={'init_cluster.cluster_env': 'dev'})
assert inst._deployment_mode() == 'k3d'
def test_service_mode(self):
inst = _TestableInstaller(inputs={'init_cluster.cluster_env': 'service'})
assert inst._deployment_mode() == 'k3s'
def test_prod_mode(self):
inst = _TestableInstaller(inputs={'init_cluster.cluster_env': 'prod'})
assert inst._deployment_mode() == 'k8s'
def test_empty_mode(self):
inst = _TestableInstaller(inputs={'init_cluster.cluster_env': ''})
result = inst._deployment_mode()
assert result == '' or result is not None
# ---------------------------------------------------------------------------
# _secret_namespace
# ---------------------------------------------------------------------------
class TestSecretNamespace:
def test_from_db_namespace(self):
inst = _TestableInstaller(inputs={'init_password.db_namespace': 'my-ns'})
assert inst._secret_namespace() == 'my-ns'
def test_from_env_namespace(self):
inst = _TestableInstaller(inputs={'env_setup.NAMESPACE': 'env-ns'})
assert inst._secret_namespace() == 'env-ns'
def test_from_cfg_data(self):
inst = _TestableInstaller()
inst.prole_cfg_data['Global']['NAMESPACE'] = 'cfg-ns'
assert inst._secret_namespace() == 'cfg-ns'
def test_default(self):
inst = _TestableInstaller()
assert inst._secret_namespace() == 'default'
def test_priority_db_over_env(self):
inst = _TestableInstaller(inputs={
'init_password.db_namespace': 'db-ns',
'env_setup.NAMESPACE': 'env-ns',
})
assert inst._secret_namespace() == 'db-ns'
def test_strips_whitespace(self):
inst = _TestableInstaller(inputs={'init_password.db_namespace': ' my-ns '})
assert inst._secret_namespace() == 'my-ns'
# ---------------------------------------------------------------------------
# _service_namespace
# ---------------------------------------------------------------------------
class TestServiceNamespace:
def test_from_cfg_data(self):
inst = _TestableInstaller()
inst.prole_cfg_data['Global']['SERVICE_NAMESPACE'] = 'svc-ns'
assert inst._service_namespace() == 'svc-ns'
def test_from_env_var(self):
inst = _TestableInstaller()
with mock.patch.dict(os.environ, {'SERVICE_NAMESPACE': 'env-svc-ns'}):
assert inst._service_namespace() == 'env-svc-ns'
def test_default(self):
inst = _TestableInstaller()
with mock.patch.dict(os.environ, {}, clear=False):
os.environ.pop('SERVICE_NAMESPACE', None)
assert inst._service_namespace() == 'default'
# ---------------------------------------------------------------------------
# _argocd_namespace / _registry_namespace
# ---------------------------------------------------------------------------
class _TestableSilentInstaller(ProleSilentInstaller):
"""Testable subclass of ProleSilentInstaller."""
def __init__(self, inputs=None, project_root=None):
self._inputs_dict = inputs or {}
self.project_root = project_root or Path('/tmp/fake-prole')
self.controller = MagicMock()
self.cfg_path = None
self.silent = True
self._init_shared_state()
def _get_input(self, key: str, default: str | None = None) -> str:
return self._inputs_dict.get(key, default or '')
class TestNamespaceHelpers:
def test_argocd_from_cfg(self):
inst = _TestableSilentInstaller()
inst.prole_cfg_data['Global']['ARGOCD_NAMESPACE'] = 'argo-ns'
assert inst._argocd_namespace() == 'argo-ns'
def test_argocd_from_env(self):
inst = _TestableSilentInstaller()
with mock.patch.dict(os.environ, {'ARGOCD_NAMESPACE': 'env-argo'}):
assert inst._argocd_namespace() == 'env-argo'
def test_argocd_default(self):
inst = _TestableSilentInstaller()
with mock.patch.dict(os.environ, {}, clear=False):
os.environ.pop('ARGOCD_NAMESPACE', None)
assert inst._argocd_namespace() == 'argocd'
def test_registry_from_cfg(self):
inst = _TestableSilentInstaller()
inst.prole_cfg_data['Global']['REGISTRY_NAMESPACE'] = 'reg-ns'
assert inst._registry_namespace() == 'reg-ns'
def test_registry_from_env(self):
inst = _TestableSilentInstaller()
with mock.patch.dict(os.environ, {'REGISTRY_NAMESPACE': 'env-reg'}):
assert inst._registry_namespace() == 'env-reg'
def test_registry_default(self):
inst = _TestableSilentInstaller()
with mock.patch.dict(os.environ, {}, clear=False):
os.environ.pop('REGISTRY_NAMESPACE', None)
assert inst._registry_namespace() == 'default'
# ---------------------------------------------------------------------------
# _read_k3s_cfg_values
# ---------------------------------------------------------------------------
class TestReadK3sCfgValues:
def test_no_cfg_path(self):
inst = _TestableInstaller()
env, url, token = inst._read_k3s_cfg_values(None)
# With no cfg, should return empty/defaults
assert isinstance(env, str)
assert isinstance(url, str)
assert isinstance(token, str)
def test_with_cfg_path(self, tmp_path):
import configparser
cfg = configparser.ConfigParser(interpolation=None)
cfg.optionxform = str
cfg.add_section('Initialize Cluster')
cfg.set('Initialize Cluster', 'ENVIRONMENT', 'service')
cfg.set('Initialize Cluster', 'K3S_SERVER_URL', 'https://10.0.0.1:6443')
cfg.set('Initialize Cluster', 'K3S_TOKEN', 'mytoken')
cfg_path = tmp_path / 'prole.cfg'
with open(cfg_path, 'w') as f:
cfg.write(f)
inst = _TestableInstaller()
env, url, token = inst._read_k3s_cfg_values(cfg_path)
assert env == 'service'
assert url == 'https://10.0.0.1:6443'
assert token == 'mytoken'
# ---------------------------------------------------------------------------
# _run_script
# ---------------------------------------------------------------------------
class TestRunScript:
def test_run_script_delegates(self):
inst = _TestableInstaller(inputs={'init_cluster.cluster_env': 'dev'})
inst.controller.run_script.return_value = 0
rc = inst._run_script("test_script.sh", args=["arg1"])
assert rc == 0
inst.controller.run_script.assert_called_once()
def test_run_script_adds_mode(self):
inst = _TestableInstaller(inputs={'init_cluster.cluster_env': 'dev'})
inst.controller.run_script.return_value = 0
inst._run_script("init_common_services.sh", args=["update"])
call_kwargs = inst.controller.run_script.call_args
args_passed = call_kwargs[1].get('args', call_kwargs[0][1] if len(call_kwargs[0]) > 1 else [])
assert '--mode' in args_passed
assert 'k3d' in args_passed
def test_run_script_no_mode_for_non_init(self):
inst = _TestableInstaller(inputs={'init_cluster.cluster_env': 'dev'})
inst.controller.run_script.return_value = 0
inst._run_script("build-a-bao.sh", args=[])
call_kwargs = inst.controller.run_script.call_args
args_passed = call_kwargs[1].get('args', call_kwargs[0][1] if len(call_kwargs[0]) > 1 else [])
assert '--mode' not in args_passed