Add governed organization template upgrades
This commit is contained in:
@@ -22,6 +22,7 @@ from govoplan_core.core.organizations import (
|
||||
organization_lifecycle_event_type,
|
||||
)
|
||||
from govoplan_core.core.events import (
|
||||
EventActorRef,
|
||||
EventObjectRef,
|
||||
EventTenantRef,
|
||||
PlatformEvent,
|
||||
@@ -34,6 +35,7 @@ from govoplan_organizations.backend.db.models import (
|
||||
OrganizationModelInstantiation,
|
||||
OrganizationModelTemplate,
|
||||
OrganizationModelTemplateVersion,
|
||||
OrganizationModelUpgrade,
|
||||
OrganizationRelation,
|
||||
OrganizationRelationType,
|
||||
OrganizationTenantSettings,
|
||||
@@ -48,6 +50,12 @@ from .schemas import (
|
||||
FunctionTypeUpdateRequest,
|
||||
FunctionUpdateRequest,
|
||||
OrganizationModelInstantiationItem,
|
||||
OrganizationModelUpgradeApplyRequest,
|
||||
OrganizationModelUpgradeApplyResponse,
|
||||
OrganizationModelUpgradeCancelRequest,
|
||||
OrganizationModelUpgradeItem,
|
||||
OrganizationModelUpgradeListResponse,
|
||||
OrganizationModelUpgradePreviewRequest,
|
||||
OrganizationFunctionItem,
|
||||
OrganizationFunctionTypeItem,
|
||||
OrganizationModelResponse,
|
||||
@@ -80,6 +88,14 @@ from govoplan_organizations.backend.templates import (
|
||||
canonical_template_definition,
|
||||
instantiate_template_version,
|
||||
)
|
||||
from govoplan_organizations.backend.upgrades import (
|
||||
OrganizationUpgradeError,
|
||||
apply_model_upgrade,
|
||||
cancel_model_upgrade,
|
||||
create_model_upgrade_preview,
|
||||
current_model_instantiation,
|
||||
list_model_upgrades,
|
||||
)
|
||||
|
||||
|
||||
router = APIRouter(prefix="/organizations", tags=["organizations"])
|
||||
@@ -475,6 +491,74 @@ def _instantiation_item(
|
||||
)
|
||||
|
||||
|
||||
def _upgrade_item(item: OrganizationModelUpgrade) -> OrganizationModelUpgradeItem:
|
||||
return OrganizationModelUpgradeItem(
|
||||
id=item.id,
|
||||
tenant_id=item.tenant_id,
|
||||
template_id=item.template_id,
|
||||
source_instantiation_id=item.source_instantiation_id,
|
||||
source_template_version_id=item.source_template_version_id,
|
||||
target_template_version_id=item.target_template_version_id,
|
||||
status=item.status,
|
||||
revision=item.revision,
|
||||
base_definition_sha256=item.base_definition_sha256,
|
||||
local_definition_sha256=item.local_definition_sha256,
|
||||
target_definition_sha256=item.target_definition_sha256,
|
||||
preview=item.preview,
|
||||
decisions=item.decisions,
|
||||
requested_by_account_id=item.requested_by_account_id,
|
||||
applied_by_account_id=item.applied_by_account_id,
|
||||
cancelled_by_account_id=item.cancelled_by_account_id,
|
||||
applied_at=item.applied_at,
|
||||
cancelled_at=item.cancelled_at,
|
||||
provenance=dict(item.provenance or {}),
|
||||
created_at=item.created_at,
|
||||
updated_at=item.updated_at,
|
||||
)
|
||||
|
||||
|
||||
def _emit_model_upgrade_event(
|
||||
session: Session,
|
||||
principal: ApiPrincipal,
|
||||
item: OrganizationModelUpgrade,
|
||||
action: str,
|
||||
) -> None:
|
||||
emit_platform_event(
|
||||
session,
|
||||
PlatformEvent(
|
||||
type=f"organizations.model_upgrade.{action}.v1",
|
||||
module_id="organizations",
|
||||
tenant=EventTenantRef(id=item.tenant_id),
|
||||
actor=EventActorRef(type="account", id=principal.account_id),
|
||||
subject=EventObjectRef(type="organization_model_upgrade", id=item.id),
|
||||
resource=EventObjectRef(
|
||||
type="organization_model_template_version",
|
||||
id=item.target_template_version_id,
|
||||
),
|
||||
payload={
|
||||
"schema_version": "1",
|
||||
"tenant_id": item.tenant_id,
|
||||
"upgrade_id": item.id,
|
||||
"status": item.status,
|
||||
"revision": item.revision,
|
||||
"source_template_version_id": item.source_template_version_id,
|
||||
"target_template_version_id": item.target_template_version_id,
|
||||
"requires_decisions": int(
|
||||
dict(item.preview or {}).get("requires_decisions", 0)
|
||||
),
|
||||
"blocking_invalid_references": int(
|
||||
dict(item.preview or {}).get(
|
||||
"blocking_invalid_references", 0
|
||||
)
|
||||
),
|
||||
"decision_count": len(dict(item.decisions or {})),
|
||||
"silent_mutation": False,
|
||||
},
|
||||
classification="internal",
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def _template_version(
|
||||
session: Session,
|
||||
template_id: str,
|
||||
@@ -742,6 +826,172 @@ def instantiate_organization_model_template(
|
||||
return _instantiation_item(instantiation)
|
||||
|
||||
|
||||
@router.get(
|
||||
"/model-upgrades",
|
||||
response_model=OrganizationModelUpgradeListResponse,
|
||||
)
|
||||
def list_organization_model_upgrades(
|
||||
session: Session = Depends(get_session),
|
||||
principal: ApiPrincipal = Depends(require_any_scope(*ORG_READ_SCOPES)),
|
||||
) -> OrganizationModelUpgradeListResponse:
|
||||
current = current_model_instantiation(
|
||||
session,
|
||||
tenant_id=principal.tenant_id,
|
||||
)
|
||||
return OrganizationModelUpgradeListResponse(
|
||||
current_instantiation=(
|
||||
_instantiation_item(current) if current is not None else None
|
||||
),
|
||||
upgrades=[
|
||||
_upgrade_item(item)
|
||||
for item in list_model_upgrades(
|
||||
session,
|
||||
tenant_id=principal.tenant_id,
|
||||
)
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/model-upgrades/preview",
|
||||
response_model=OrganizationModelUpgradeItem,
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
)
|
||||
def preview_organization_model_upgrade(
|
||||
payload: OrganizationModelUpgradePreviewRequest,
|
||||
session: Session = Depends(get_session),
|
||||
principal: ApiPrincipal = Depends(
|
||||
require_any_scope(*ORG_MODEL_WRITE_SCOPES)
|
||||
),
|
||||
) -> OrganizationModelUpgradeItem:
|
||||
target = session.get(
|
||||
OrganizationModelTemplateVersion,
|
||||
payload.target_template_version_id,
|
||||
)
|
||||
if target is None:
|
||||
raise _not_found("Organization model template version")
|
||||
try:
|
||||
item = create_model_upgrade_preview(
|
||||
session,
|
||||
tenant_id=principal.tenant_id,
|
||||
target_version=target,
|
||||
actor_account_id=principal.account_id,
|
||||
idempotency_key=payload.idempotency_key,
|
||||
)
|
||||
_emit_model_upgrade_event(session, principal, item, "previewed")
|
||||
session.commit()
|
||||
session.refresh(item)
|
||||
except OrganizationUpgradeError as exc:
|
||||
session.rollback()
|
||||
raise _conflict(str(exc)) from exc
|
||||
return _upgrade_item(item)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/model-upgrades/{upgrade_id}/cancel",
|
||||
response_model=OrganizationModelUpgradeItem,
|
||||
)
|
||||
def cancel_organization_model_upgrade(
|
||||
upgrade_id: str,
|
||||
payload: OrganizationModelUpgradeCancelRequest,
|
||||
session: Session = Depends(get_session),
|
||||
principal: ApiPrincipal = Depends(
|
||||
require_any_scope(*ORG_MODEL_WRITE_SCOPES)
|
||||
),
|
||||
) -> OrganizationModelUpgradeItem:
|
||||
try:
|
||||
item = cancel_model_upgrade(
|
||||
session,
|
||||
tenant_id=principal.tenant_id,
|
||||
upgrade_id=upgrade_id,
|
||||
expected_revision=payload.expected_revision,
|
||||
actor_account_id=principal.account_id,
|
||||
)
|
||||
_emit_model_upgrade_event(session, principal, item, "cancelled")
|
||||
session.commit()
|
||||
session.refresh(item)
|
||||
except OrganizationUpgradeError as exc:
|
||||
session.rollback()
|
||||
raise _conflict(str(exc)) from exc
|
||||
return _upgrade_item(item)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/model-upgrades/{upgrade_id}/apply",
|
||||
response_model=OrganizationModelUpgradeApplyResponse,
|
||||
)
|
||||
def apply_organization_model_upgrade(
|
||||
upgrade_id: str,
|
||||
payload: OrganizationModelUpgradeApplyRequest,
|
||||
session: Session = Depends(get_session),
|
||||
principal: ApiPrincipal = Depends(
|
||||
require_any_scope(*ORG_MODEL_WRITE_SCOPES)
|
||||
),
|
||||
) -> OrganizationModelUpgradeApplyResponse:
|
||||
approval, control_target, _value = _ensure_organization_change_allowed(
|
||||
session,
|
||||
principal,
|
||||
tenant_id=principal.tenant_id,
|
||||
resource_type="model_upgrade",
|
||||
operation="applied",
|
||||
payload=payload,
|
||||
resource_id=upgrade_id,
|
||||
)
|
||||
before = _upgrade_item(
|
||||
_get_tenant_row(
|
||||
session,
|
||||
OrganizationModelUpgrade,
|
||||
upgrade_id,
|
||||
principal.tenant_id,
|
||||
"Organization model upgrade",
|
||||
)
|
||||
).model_dump(mode="json")
|
||||
try:
|
||||
item, instantiation = apply_model_upgrade(
|
||||
session,
|
||||
tenant_id=principal.tenant_id,
|
||||
upgrade_id=upgrade_id,
|
||||
expected_revision=payload.expected_revision,
|
||||
decisions={
|
||||
key: value.model_dump(exclude_none=True)
|
||||
for key, value in payload.decisions.items()
|
||||
},
|
||||
actor_account_id=principal.account_id,
|
||||
)
|
||||
invalidate_auth_principals(
|
||||
session,
|
||||
tenant_id=principal.tenant_id,
|
||||
source_module="organizations",
|
||||
resource_type="organization_model_upgrade",
|
||||
resource_id=item.id,
|
||||
)
|
||||
_emit_model_upgrade_event(session, principal, item, "applied")
|
||||
response = OrganizationModelUpgradeApplyResponse(
|
||||
upgrade=_upgrade_item(item),
|
||||
instantiation=_instantiation_item(instantiation),
|
||||
)
|
||||
if approval is None:
|
||||
session.commit()
|
||||
else:
|
||||
_record_organization_change_applied(
|
||||
session,
|
||||
principal,
|
||||
approval=approval,
|
||||
target=control_target,
|
||||
before=before,
|
||||
after=response.model_dump(mode="json"),
|
||||
)
|
||||
session.refresh(item)
|
||||
session.refresh(instantiation)
|
||||
except OrganizationUpgradeError as exc:
|
||||
session.rollback()
|
||||
raise _conflict(str(exc)) from exc
|
||||
return OrganizationModelUpgradeApplyResponse(
|
||||
upgrade=_upgrade_item(item),
|
||||
instantiation=_instantiation_item(instantiation),
|
||||
)
|
||||
|
||||
|
||||
@router.get("/model", response_model=OrganizationModelResponse)
|
||||
def get_organization_model(
|
||||
session: Session = Depends(get_session),
|
||||
|
||||
Reference in New Issue
Block a user