Fence and reconcile module lifecycle effects
This commit is contained in:
+105
-3
@@ -20,7 +20,7 @@ from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import patch
|
||||
|
||||
from sqlalchemy import Column, Integer, MetaData, Table, create_engine, insert, inspect
|
||||
from sqlalchemy import Column, Integer, MetaData, Table, create_engine, insert, inspect, select
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
# Keep the default app import side effect from bootstrapping a development DB.
|
||||
@@ -93,6 +93,13 @@ from govoplan_core.core.configuration_packages import (
|
||||
validate_configuration_package_catalog,
|
||||
)
|
||||
from govoplan_core.core.module_license import issue_module_license, module_license_decision, module_license_diagnostics, validate_module_license
|
||||
from govoplan_core.core.recovery import (
|
||||
RecoveryCheckpoint,
|
||||
RecoveryOperation,
|
||||
RecoveryStatus,
|
||||
verify_recovery_evidence_chain,
|
||||
)
|
||||
from govoplan_core.core.runtime_coordination import DistributedLease
|
||||
from govoplan_core.core.module_package_catalog import (
|
||||
module_package_catalog,
|
||||
record_module_package_catalog_acceptance,
|
||||
@@ -2607,7 +2614,15 @@ finally:
|
||||
settings = _settings(root)
|
||||
configure_database(settings.database_url)
|
||||
database = get_database()
|
||||
Base.metadata.create_all(bind=database.engine, tables=[SystemSettings.__table__])
|
||||
Base.metadata.create_all(
|
||||
bind=database.engine,
|
||||
tables=[
|
||||
SystemSettings.__table__,
|
||||
DistributedLease.__table__,
|
||||
RecoveryOperation.__table__,
|
||||
RecoveryCheckpoint.__table__,
|
||||
],
|
||||
)
|
||||
metadata = MetaData()
|
||||
table = Table("retirement_example", metadata, Column("id", Integer, primary_key=True))
|
||||
metadata.create_all(bind=database.engine)
|
||||
@@ -2658,6 +2673,9 @@ finally:
|
||||
database_url=settings.database_url,
|
||||
runtime_dir=root / "installer",
|
||||
)
|
||||
recovery = session.execute(select(RecoveryOperation)).scalar_one()
|
||||
self.assertEqual(RecoveryStatus.SUCCEEDED.value, recovery.status)
|
||||
self.assertTrue(verify_recovery_evidence_chain(session, recovery.id))
|
||||
|
||||
self.assertEqual("applied", result.status)
|
||||
self.assertFalse(inspect(database.engine).has_table("retirement_example"))
|
||||
@@ -2813,7 +2831,15 @@ finally:
|
||||
settings = _settings(root)
|
||||
configure_database(settings.database_url)
|
||||
database = get_database()
|
||||
Base.metadata.create_all(bind=database.engine, tables=[SystemSettings.__table__])
|
||||
Base.metadata.create_all(
|
||||
bind=database.engine,
|
||||
tables=[
|
||||
SystemSettings.__table__,
|
||||
DistributedLease.__table__,
|
||||
RecoveryOperation.__table__,
|
||||
RecoveryCheckpoint.__table__,
|
||||
],
|
||||
)
|
||||
|
||||
def fake_run(*_args, **kwargs):
|
||||
argv = tuple(_args[0]) if _args else ()
|
||||
@@ -2846,11 +2872,78 @@ finally:
|
||||
|
||||
restored_desired = saved_desired_enabled_modules(session, ("tenancy", "access"))
|
||||
restored_plan = saved_module_install_plan(session)
|
||||
recovery = session.execute(select(RecoveryOperation)).scalar_one()
|
||||
self.assertEqual(RecoveryStatus.RECOVERED.value, recovery.status)
|
||||
self.assertTrue(verify_recovery_evidence_chain(session, recovery.id))
|
||||
|
||||
self.assertEqual("rolled-back", result.status)
|
||||
self.assertEqual(("tenancy", "access"), restored_desired)
|
||||
self.assertEqual(("planned",), tuple(item.status for item in restored_plan.items))
|
||||
|
||||
def test_module_installer_blocks_after_unresolved_package_effect(self) -> None:
|
||||
root = Path(tempfile.mkdtemp(prefix="govoplan-installer-unresolved-", dir=_TEST_ROOT))
|
||||
settings = _settings(root)
|
||||
configure_database(settings.database_url)
|
||||
database = get_database()
|
||||
Base.metadata.create_all(
|
||||
bind=database.engine,
|
||||
tables=[
|
||||
SystemSettings.__table__,
|
||||
DistributedLease.__table__,
|
||||
RecoveryOperation.__table__,
|
||||
RecoveryCheckpoint.__table__,
|
||||
],
|
||||
)
|
||||
|
||||
def fail_package_install(*args, **_kwargs):
|
||||
argv = tuple(args[0]) if args else ()
|
||||
if any("govoplan-example==0.1.4" in str(item) for item in argv):
|
||||
return SimpleNamespace(returncode=1, stdout="", stderr="install failed")
|
||||
return SimpleNamespace(returncode=0, stdout="", stderr="")
|
||||
|
||||
with database.session() as session:
|
||||
save_maintenance_mode(session, MaintenanceMode(enabled=True))
|
||||
plan = save_module_install_plan(session, [{
|
||||
"module_id": "example",
|
||||
"action": "install",
|
||||
"python_package": "govoplan-example",
|
||||
"python_ref": "govoplan-example==0.1.4",
|
||||
}])
|
||||
session.commit()
|
||||
|
||||
with patch(
|
||||
"govoplan_core.core.module_installer.subprocess.run",
|
||||
side_effect=fail_package_install,
|
||||
):
|
||||
result = run_module_install_plan(
|
||||
session=session,
|
||||
plan=plan,
|
||||
available=available_module_manifests(),
|
||||
current_enabled=("tenancy", "access"),
|
||||
desired_enabled=("tenancy", "access"),
|
||||
database_url=settings.database_url,
|
||||
runtime_dir=root / "installer",
|
||||
)
|
||||
|
||||
self.assertEqual("failed", result.status)
|
||||
recovery = session.execute(select(RecoveryOperation)).scalar_one()
|
||||
self.assertEqual(RecoveryStatus.RECOVERY_REQUIRED.value, recovery.status)
|
||||
self.assertTrue(verify_recovery_evidence_chain(session, recovery.id))
|
||||
|
||||
with self.assertRaisesRegex(
|
||||
module_installer_module.ModuleInstallerError,
|
||||
"already recovery_required",
|
||||
):
|
||||
run_module_install_plan(
|
||||
session=session,
|
||||
plan=plan,
|
||||
available=available_module_manifests(),
|
||||
current_enabled=("tenancy", "access"),
|
||||
desired_enabled=("tenancy", "access"),
|
||||
database_url=settings.database_url,
|
||||
runtime_dir=root / "installer",
|
||||
)
|
||||
|
||||
def test_module_installer_external_database_backup_command_is_recorded(self) -> None:
|
||||
root = Path(tempfile.mkdtemp(prefix="govoplan-installer-external-backup-", dir=_TEST_ROOT))
|
||||
settings = _settings(root)
|
||||
@@ -4216,6 +4309,15 @@ finally:
|
||||
app, _settings_obj = self._app_for_modules(())
|
||||
lifecycle = getattr(app.state, "govoplan_lifecycle", None)
|
||||
self.assertIsNotNone(lifecycle)
|
||||
database = get_database()
|
||||
Base.metadata.create_all(
|
||||
bind=database.engine,
|
||||
tables=[
|
||||
DistributedLease.__table__,
|
||||
RecoveryOperation.__table__,
|
||||
RecoveryCheckpoint.__table__,
|
||||
],
|
||||
)
|
||||
|
||||
with TestClient(app) as client:
|
||||
response = client.get("/api/v1/platform/modules")
|
||||
|
||||
Reference in New Issue
Block a user