"""Record cloud-side provider state on decommissioned scopes. Revision ID: 090 Revises: 089 Create Date: 2026-04-29 Adds ``cmdb_cloud_scopes.decommission_provider_state`` so the UI and audit log can render the post-close cloud-side state — ``pending_closure`` (AWS, Aliyun ResourceAccount), ``cancelled`` (Azure), ``delete_requested`` (GCP), ``removed_from_org`` (Aliyun CloudAccount unlink), ``users_evacuated`` (Tencent / UCloud / AWS standalone IAM), ``not_in_org`` / ``not_found`` (already-closed idempotent paths). Companion to PR #112's ``decommissioned_at``/``_by``/``_reason`` columns. PR #112 was Helix-side bookkeeping only; this migration ships alongside the driver-level ``close_scope_on_provider`` rollout where decommission actually triggers the provider-side close API and we need a column to capture which terminal state the cloud reported. Idempotent: the column add is guarded so re-runs don't fail. """ from __future__ import annotations import sqlalchemy as sa from alembic import op revision = "090" down_revision = "089" branch_labels = None depends_on = None def _column_exists(table: str, column: str) -> bool: bind = op.get_bind() insp = sa.inspect(bind) return any(c["name"] == column for c in insp.get_columns(table)) def upgrade() -> None: if not _column_exists("cmdb_cloud_scopes", "decommission_provider_state"): op.add_column( "cmdb_cloud_scopes", sa.Column( "decommission_provider_state", sa.String(length=64), nullable=True, ), ) # Companion: track per-IAM-user revocation independently of soft # delete. ``deleted_at`` already means "row hidden" for sync # housekeeping; ``revoked_at`` means Helix actively removed the # user on the cloud side (offboarding / scope-decommission # evacuation) and the IAM users page should still render the row # with a status pill so admins can see what happened. if not _column_exists("cmdb_iam_users", "revoked_at"): op.add_column( "cmdb_iam_users", sa.Column( "revoked_at", sa.DateTime(timezone=True), nullable=True, ), ) if not _column_exists("cmdb_iam_users", "revoke_reason"): op.add_column( "cmdb_iam_users", sa.Column( "revoke_reason", sa.String(length=64), nullable=True, ), ) def downgrade() -> None: if _column_exists("cmdb_iam_users", "revoke_reason"): op.drop_column("cmdb_iam_users", "revoke_reason") if _column_exists("cmdb_iam_users", "revoked_at"): op.drop_column("cmdb_iam_users", "revoked_at") if _column_exists("cmdb_cloud_scopes", "decommission_provider_state"): op.drop_column("cmdb_cloud_scopes", "decommission_provider_state")