Release v0.1.7
This commit is contained in:
@@ -31,6 +31,7 @@ from govoplan_core.core.configuration_control import (
|
||||
)
|
||||
from govoplan_core.core.module_management import (
|
||||
PROTECTED_MODULES,
|
||||
ModuleInstallPlan,
|
||||
ModuleInstallPlanItem,
|
||||
ModuleManagementError,
|
||||
configured_enabled_modules,
|
||||
@@ -49,6 +50,7 @@ from govoplan_core.core.module_installer import (
|
||||
default_installer_runtime_dir,
|
||||
list_module_installer_runs,
|
||||
list_module_installer_requests,
|
||||
module_install_catalog_companion_module_ids,
|
||||
module_installer_daemon_status,
|
||||
module_install_preflight,
|
||||
module_installer_lock_status,
|
||||
@@ -429,17 +431,27 @@ def _webui_root() -> Path | None:
|
||||
|
||||
|
||||
def _upsert_install_plan_item(session: Session, item: ModuleInstallPlanItem):
|
||||
return _upsert_install_plan_items(session, [item])
|
||||
|
||||
|
||||
def _upsert_install_plan_items(session: Session, items: list[ModuleInstallPlanItem]):
|
||||
existing = saved_module_install_plan(session)
|
||||
replacement_ids = {item.module_id for item in items if item.status == "planned"}
|
||||
retained = [
|
||||
current
|
||||
for current in existing.items
|
||||
if not (current.status == "planned" and current.module_id == item.module_id)
|
||||
if not (current.status == "planned" and current.module_id in replacement_ids)
|
||||
]
|
||||
return save_module_install_plan(session, [*retained, item])
|
||||
return save_module_install_plan(session, [*retained, *items])
|
||||
|
||||
|
||||
def _catalog_plan_item(module_id: str, available_module_ids: set[str]) -> tuple[ModuleInstallPlanItem, dict[str, object]]:
|
||||
result = validate_module_package_catalog()
|
||||
def _catalog_plan_item(
|
||||
module_id: str,
|
||||
available_module_ids: set[str],
|
||||
*,
|
||||
validation: dict[str, object] | None = None,
|
||||
) -> tuple[ModuleInstallPlanItem, dict[str, object]]:
|
||||
result = validation or validate_module_package_catalog()
|
||||
if not result.get("valid"):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_422_UNPROCESSABLE_CONTENT,
|
||||
@@ -801,7 +813,19 @@ def plan_module_install_from_catalog(
|
||||
lifecycle = getattr(request.app.state, "govoplan_lifecycle", None)
|
||||
available = dict(lifecycle.available_modules) if isinstance(lifecycle, ModuleLifecycleManager) else available_module_manifests(ignore_load_errors=True)
|
||||
item, validation = _catalog_plan_item(module_id, set(available))
|
||||
plan = _upsert_install_plan_item(session, item)
|
||||
existing = saved_module_install_plan(session)
|
||||
retained = [
|
||||
current
|
||||
for current in existing.items
|
||||
if not (current.status == "planned" and current.module_id == item.module_id)
|
||||
]
|
||||
candidate_plan = ModuleInstallPlan(items=tuple([*retained, item]))
|
||||
companion_ids = module_install_catalog_companion_module_ids(candidate_plan, available, validation=validation)
|
||||
companion_items = [
|
||||
_catalog_plan_item(companion_id, set(available), validation=validation)[0]
|
||||
for companion_id in companion_ids
|
||||
]
|
||||
plan = _upsert_install_plan_items(session, [item, *companion_items])
|
||||
record_module_package_catalog_acceptance(validation)
|
||||
except ModuleManagementError as exc:
|
||||
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_CONTENT, detail=str(exc)) from exc
|
||||
@@ -816,6 +840,7 @@ def plan_module_install_from_catalog(
|
||||
module_id=module_id,
|
||||
outcome="planned",
|
||||
items=[item.as_dict() for item in plan.items],
|
||||
companion_module_ids=list(companion_ids),
|
||||
catalog_validation={
|
||||
"valid": validation.get("valid"),
|
||||
"channel": validation.get("channel"),
|
||||
@@ -825,7 +850,10 @@ def plan_module_install_from_catalog(
|
||||
)
|
||||
session.commit()
|
||||
planned_action = "update" if item.action == "update" else "install"
|
||||
return _module_install_plan_response(session, request, notes=[f"Catalog {planned_action} entry planned for {module_id}."])
|
||||
notes = [f"Catalog {planned_action} entry planned for {module_id}."]
|
||||
if companion_ids:
|
||||
notes.append("Required companion catalog entries added: " + ", ".join(companion_ids))
|
||||
return _module_install_plan_response(session, request, notes=notes)
|
||||
|
||||
|
||||
@router.post("/system/modules/{module_id}/uninstall-plan", response_model=ModuleInstallPlanResponse)
|
||||
|
||||
@@ -182,6 +182,7 @@ class ModuleInstallPlanItem(BaseModel):
|
||||
python_ref: str | None = Field(default=None, max_length=1000)
|
||||
webui_package: str | None = Field(default=None, max_length=200)
|
||||
webui_ref: str | None = Field(default=None, max_length=1000)
|
||||
data_safety_acknowledged: bool = False
|
||||
destroy_data: bool = False
|
||||
status: Literal["planned", "applied", "blocked"] = "planned"
|
||||
notes: str | None = Field(default=None, max_length=1000)
|
||||
@@ -201,6 +202,63 @@ class ModuleInstallChecklistItem(BaseModel):
|
||||
detail: str | None = None
|
||||
|
||||
|
||||
class ModuleInstallTargetItem(BaseModel):
|
||||
module_id: str
|
||||
action: Literal["install", "update", "uninstall"]
|
||||
source: Literal["manual", "catalog"]
|
||||
current_version: str | None = None
|
||||
target_version: str | None = None
|
||||
python_package: str | None = None
|
||||
python_ref: str | None = None
|
||||
webui_package: str | None = None
|
||||
webui_ref: str | None = None
|
||||
migration_safety: Literal["automatic", "requires_review", "forward_only", "destructive"] = "automatic"
|
||||
migration_notes: str | None = None
|
||||
current_version_min: str | None = None
|
||||
current_version_max_exclusive: str | None = None
|
||||
bridge_release: bool = False
|
||||
bridge_notes: str | None = None
|
||||
allow_downgrade: bool = False
|
||||
allow_same_version: bool = False
|
||||
recovery_tested: bool = False
|
||||
recovery_notes: str | None = None
|
||||
data_safety_acknowledged: bool = False
|
||||
|
||||
|
||||
class ModuleMigrationPlanStep(BaseModel):
|
||||
module_id: str
|
||||
action: Literal["install", "update", "uninstall"]
|
||||
phase: Literal["upgrade", "retirement"]
|
||||
source: Literal["manifest", "catalog", "pending"]
|
||||
has_migration_metadata: bool = False
|
||||
metadata_pending: bool = False
|
||||
migration_safety: Literal["automatic", "requires_review", "forward_only", "destructive"] = "automatic"
|
||||
current_version: str | None = None
|
||||
target_version: str | None = None
|
||||
reason: str | None = None
|
||||
|
||||
|
||||
class ModuleMigrationTaskPlanItem(BaseModel):
|
||||
module_id: str
|
||||
task_id: str
|
||||
phase: Literal["pre_migration_check", "pre_migration_prepare", "post_migration_backfill", "post_migration_verify"]
|
||||
summary: str
|
||||
task_version: str = "1"
|
||||
safety: Literal["automatic", "requires_review", "forward_only", "destructive"] = "automatic"
|
||||
idempotent: bool = True
|
||||
timeout_seconds: int | None = None
|
||||
source: Literal["manifest", "catalog", "pending"] = "manifest"
|
||||
has_executor: bool = False
|
||||
metadata_pending: bool = False
|
||||
|
||||
|
||||
class ModuleMigrationExecutionPlan(BaseModel):
|
||||
enabled_modules: list[str] = Field(default_factory=list)
|
||||
requires_database_migration: bool = False
|
||||
steps: list[ModuleMigrationPlanStep] = Field(default_factory=list)
|
||||
tasks: list[ModuleMigrationTaskPlanItem] = Field(default_factory=list)
|
||||
|
||||
|
||||
class ModuleInstallPreflightResponse(BaseModel):
|
||||
allowed: bool
|
||||
maintenance_mode: bool
|
||||
@@ -210,6 +268,8 @@ class ModuleInstallPreflightResponse(BaseModel):
|
||||
rollback_commands: list[str] = Field(default_factory=list)
|
||||
issues: list[ModuleInstallPreflightIssue] = Field(default_factory=list)
|
||||
checklist: list[ModuleInstallChecklistItem] = Field(default_factory=list)
|
||||
target_plan: list[ModuleInstallTargetItem] = Field(default_factory=list)
|
||||
migration_plan: ModuleMigrationExecutionPlan = Field(default_factory=ModuleMigrationExecutionPlan)
|
||||
|
||||
|
||||
class ModuleInstallPlanResponse(BaseModel):
|
||||
@@ -328,6 +388,20 @@ class ModulePackageCatalogItem(BaseModel):
|
||||
description: str | None = None
|
||||
version: str | None = None
|
||||
action: Literal["install", "update", "uninstall"] = "install"
|
||||
dependencies: list[str] = Field(default_factory=list)
|
||||
optional_dependencies: list[str] = Field(default_factory=list)
|
||||
migration_safety: Literal["automatic", "requires_review", "forward_only", "destructive"] = "automatic"
|
||||
migration_notes: str | None = None
|
||||
migration_after: list[str] = Field(default_factory=list)
|
||||
migration_before: list[str] = Field(default_factory=list)
|
||||
current_version_min: str | None = None
|
||||
current_version_max_exclusive: str | None = None
|
||||
bridge_release: bool = False
|
||||
bridge_notes: str | None = None
|
||||
allow_downgrade: bool = False
|
||||
allow_same_version: bool = False
|
||||
recovery_tested: bool = False
|
||||
recovery_notes: str | None = None
|
||||
python_package: str | None = None
|
||||
python_ref: str | None = None
|
||||
webui_package: str | None = None
|
||||
|
||||
@@ -17,7 +17,7 @@ def _route_factory(context: ModuleContext):
|
||||
manifest = ModuleManifest(
|
||||
id="admin",
|
||||
name="Admin",
|
||||
version="0.1.6",
|
||||
version="0.1.7",
|
||||
required_capabilities=(CAPABILITY_AUTH_PRINCIPAL_RESOLVER, CAPABILITY_AUTH_PERMISSION_EVALUATOR),
|
||||
route_factory=_route_factory,
|
||||
migration_spec=MigrationSpec(module_id="admin", metadata=Base.metadata),
|
||||
|
||||
Reference in New Issue
Block a user