Mark installed catalog modules as updates

This commit is contained in:
2026-07-10 23:46:26 +02:00
parent f362f3806b
commit 8c82d5b4f8
2 changed files with 14 additions and 9 deletions

View File

@@ -438,7 +438,7 @@ def _upsert_install_plan_item(session: Session, item: ModuleInstallPlanItem):
return save_module_install_plan(session, [*retained, item])
def _catalog_plan_item(module_id: str) -> tuple[ModuleInstallPlanItem, dict[str, object]]:
def _catalog_plan_item(module_id: str, available_module_ids: set[str]) -> tuple[ModuleInstallPlanItem, dict[str, object]]:
result = validate_module_package_catalog()
if not result.get("valid"):
raise HTTPException(
@@ -448,15 +448,17 @@ def _catalog_plan_item(module_id: str) -> tuple[ModuleInstallPlanItem, dict[str,
for raw_item in result.get("modules", []):
if not isinstance(raw_item, dict):
continue
if raw_item.get("module_id") != module_id or raw_item.get("action") != "install":
if raw_item.get("module_id") != module_id or raw_item.get("action") not in {"install", "update"}:
continue
raw_action = raw_item.get("action")
action = "update" if raw_action == "update" or module_id in available_module_ids else "install"
license_decision = module_license_decision(_catalog_license_features(raw_item))
if not license_decision.get("allowed"):
missing = license_decision.get("missing_features")
missing_text = ", ".join(str(item) for item in missing) if isinstance(missing, list) else "required feature"
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail=f"License does not allow installing {module_id}: {missing_text}.",
detail=f"License does not allow {action} for {module_id}: {missing_text}.",
)
notes = raw_item.get("notes") if isinstance(raw_item.get("notes"), str) else None
if license_decision.get("reason") and license_decision.get("missing_features"):
@@ -464,7 +466,7 @@ def _catalog_plan_item(module_id: str) -> tuple[ModuleInstallPlanItem, dict[str,
notes = f"{prefix}License warning: {license_decision['reason']}"
return ModuleInstallPlanItem(
module_id=str(raw_item["module_id"]),
action="install",
action=action,
source="catalog",
catalog=_catalog_plan_metadata(result),
python_package=raw_item.get("python_package") if isinstance(raw_item.get("python_package"), str) else None,
@@ -473,7 +475,7 @@ def _catalog_plan_item(module_id: str) -> tuple[ModuleInstallPlanItem, dict[str,
webui_ref=raw_item.get("webui_ref") if isinstance(raw_item.get("webui_ref"), str) else None,
notes=notes,
), result
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"Catalog install entry not found: {module_id}")
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"Catalog install/update entry not found: {module_id}")
def _catalog_plan_metadata(validation: dict[str, object]) -> dict[str, object]:
@@ -796,7 +798,9 @@ def plan_module_install_from_catalog(
principal: ApiPrincipal = Depends(require_scope("system:settings:write")),
):
try:
item, validation = _catalog_plan_item(module_id)
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)
record_module_package_catalog_acceptance(validation)
except ModuleManagementError as exc:
@@ -820,7 +824,8 @@ def plan_module_install_from_catalog(
),
)
session.commit()
return _module_install_plan_response(session, request, notes=[f"Catalog install entry planned for {module_id}."])
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}."])
@router.post("/system/modules/{module_id}/uninstall-plan", response_model=ModuleInstallPlanResponse)

View File

@@ -175,7 +175,7 @@ class ModuleInstallPlanItem(BaseModel):
model_config = ConfigDict(extra="forbid")
module_id: str = Field(min_length=1, max_length=120)
action: Literal["install", "uninstall"]
action: Literal["install", "update", "uninstall"]
source: Literal["manual", "catalog"] = "manual"
catalog: dict[str, Any] | None = None
python_package: str | None = Field(default=None, max_length=200)
@@ -327,7 +327,7 @@ class ModulePackageCatalogItem(BaseModel):
name: str
description: str | None = None
version: str | None = None
action: Literal["install", "uninstall"] = "install"
action: Literal["install", "update", "uninstall"] = "install"
python_package: str | None = None
python_ref: str | None = None
webui_package: str | None = None