Expose public module catalog discovery
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Mapping
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
@@ -74,7 +75,11 @@ from govoplan_core.core.module_installer_notifications import (
|
||||
installer_notification_subject,
|
||||
)
|
||||
from govoplan_core.core.module_license import module_license_decision, module_license_diagnostics
|
||||
from govoplan_core.core.module_package_catalog import record_module_package_catalog_acceptance, validate_module_package_catalog
|
||||
from govoplan_core.core.module_package_catalog import (
|
||||
record_module_package_catalog_acceptance,
|
||||
validate_module_package_catalog,
|
||||
validate_official_module_package_catalog,
|
||||
)
|
||||
from govoplan_core.core.maintenance import MAINTENANCE_ACCESS_SCOPE, MaintenanceMode, saved_maintenance_mode, save_maintenance_mode
|
||||
from govoplan_core.core.lifecycle import ModuleLifecycleManager
|
||||
from govoplan_core.core.registry import PlatformRegistry
|
||||
@@ -589,7 +594,7 @@ def _catalog_plan_item(
|
||||
*,
|
||||
validation: dict[str, object] | None = None,
|
||||
) -> tuple[ModuleInstallPlanItem, dict[str, object]]:
|
||||
result = validation or validate_module_package_catalog()
|
||||
result = validation or _module_package_catalog_validation()
|
||||
_require_valid_module_catalog(result)
|
||||
raw_item = _catalog_install_or_update_item(result, module_id)
|
||||
action = _catalog_plan_action(raw_item, module_id, available_module_ids)
|
||||
@@ -604,6 +609,7 @@ def _catalog_plan_item(
|
||||
python_ref=_catalog_string(raw_item, "python_ref"),
|
||||
webui_package=_catalog_string(raw_item, "webui_package"),
|
||||
webui_ref=_catalog_string(raw_item, "webui_ref"),
|
||||
artifact_integrity=_catalog_mapping(raw_item, "artifact_integrity"),
|
||||
notes=_catalog_plan_notes(raw_item, license_decision),
|
||||
), result
|
||||
|
||||
@@ -616,6 +622,13 @@ def _require_valid_module_catalog(result: dict[str, object]) -> None:
|
||||
)
|
||||
|
||||
|
||||
def _module_package_catalog_validation() -> dict[str, object]:
|
||||
configured = validate_module_package_catalog()
|
||||
if configured.get("configured") or configured.get("error"):
|
||||
return configured
|
||||
return validate_official_module_package_catalog()
|
||||
|
||||
|
||||
def _catalog_install_or_update_item(result: dict[str, object], module_id: str) -> dict[str, object]:
|
||||
for raw_item in result.get("modules", []):
|
||||
if not isinstance(raw_item, dict):
|
||||
@@ -659,6 +672,11 @@ def _catalog_string(raw_item: dict[str, object], field: str) -> str | None:
|
||||
return value if isinstance(value, str) else None
|
||||
|
||||
|
||||
def _catalog_mapping(raw_item: dict[str, object], field: str) -> dict[str, object] | None:
|
||||
value = raw_item.get(field)
|
||||
return dict(value) if isinstance(value, Mapping) else None
|
||||
|
||||
|
||||
def _catalog_plan_metadata(validation: dict[str, object]) -> dict[str, object]:
|
||||
metadata: dict[str, object] = {
|
||||
"source": validation.get("source") or validation.get("path"),
|
||||
@@ -688,8 +706,21 @@ def _catalog_license_features(raw_item: dict[str, object]) -> list[str]:
|
||||
return [str(item).strip() for item in value if str(item).strip()]
|
||||
|
||||
|
||||
def _module_package_catalog_item(raw_item: dict[str, object]) -> ModulePackageCatalogItem:
|
||||
def _module_package_catalog_item(
|
||||
raw_item: dict[str, object],
|
||||
available: Mapping[str, Any],
|
||||
) -> ModulePackageCatalogItem:
|
||||
payload = dict(raw_item)
|
||||
manifest = available.get(str(raw_item.get("module_id") or ""))
|
||||
installed_version = str(getattr(manifest, "version", "") or "") or None
|
||||
target_version = raw_item.get("version")
|
||||
payload["installed"] = manifest is not None
|
||||
payload["installed_version"] = installed_version
|
||||
payload["update_available"] = bool(
|
||||
installed_version
|
||||
and isinstance(target_version, str)
|
||||
and target_version != installed_version
|
||||
)
|
||||
decision = module_license_decision(_catalog_license_features(raw_item))
|
||||
payload["license_allowed"] = bool(decision.get("allowed"))
|
||||
payload["license_enforced"] = bool(decision.get("enforced"))
|
||||
@@ -1218,13 +1249,20 @@ def retry_module_install_request(
|
||||
|
||||
@router.get("/system/modules/package-catalog", response_model=ModulePackageCatalogResponse)
|
||||
def read_module_package_catalog(
|
||||
request: Request,
|
||||
principal: ApiPrincipal = Depends(require_scope("system:settings:read")),
|
||||
):
|
||||
del principal
|
||||
result = validate_module_package_catalog()
|
||||
result = _module_package_catalog_validation()
|
||||
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)
|
||||
)
|
||||
license_diagnostics = module_license_diagnostics(required_features=_catalog_required_license_features(result))
|
||||
return ModulePackageCatalogResponse(
|
||||
modules=[_module_package_catalog_item(item) for item in result["modules"] if isinstance(item, dict)],
|
||||
modules=[_module_package_catalog_item(item, available) for item in result["modules"] if isinstance(item, dict)],
|
||||
configured=bool(result["configured"]),
|
||||
valid=bool(result["valid"]),
|
||||
path=result["path"] if isinstance(result["path"], str) else None,
|
||||
|
||||
@@ -393,6 +393,9 @@ class ModulePackageCatalogItem(BaseModel):
|
||||
description: str | None = None
|
||||
version: str | None = None
|
||||
action: Literal["install", "update", "uninstall"] = "install"
|
||||
installed: bool = False
|
||||
installed_version: str | None = None
|
||||
update_available: bool = False
|
||||
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"
|
||||
|
||||
@@ -147,8 +147,9 @@ manifest = ModuleManifest(
|
||||
summary="Move a reviewed module package plan through preflight, maintenance-gated queueing, daemon execution, and durable run evidence.",
|
||||
body=(
|
||||
"The Modules administration surface projects one operator workflow: save a package plan, resolve preflight findings, enter maintenance mode with the required authority, queue a supervised installer request, and inspect the matching run record. "
|
||||
"When no deployment-specific catalog is configured, the package directory discovers the signed public GovOPlaN stable catalog and marks installed packages and available updates. Selecting an entry copies its exact signed registry identities into the plan; artifact download and digest verification happen only in the trusted installer. "
|
||||
"The stage indicator is derived from the saved plan timestamp, the latest matching request, and its run; an older request is never presented as evidence for a newer plan. "
|
||||
"Disabled queue actions name the earliest blocker, the person who can resolve it, and the plan surface where work continues. Package mutation remains outside the FastAPI process and recovery evidence remains durable in the installer ledger."
|
||||
"Disabled queue actions name the earliest blocker, the person who can resolve it, and the plan surface where work continues. Package mutation remains outside the FastAPI process and recovery evidence remains durable in the installer ledger. Shared deployments require an immutable image rollout rather than node-local mutation. Tenant entitlement and user/View visibility remain policy settings, not package lifecycle operations."
|
||||
),
|
||||
layer="configured",
|
||||
documentation_types=("admin",),
|
||||
@@ -164,6 +165,7 @@ manifest = ModuleManifest(
|
||||
"admin.module-lifecycle",
|
||||
"admin.module-lifecycle.queue-blocker",
|
||||
"admin.module-lifecycle.plan",
|
||||
"admin.module-lifecycle.catalog",
|
||||
"admin.module-lifecycle.evidence",
|
||||
],
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user