"""Build an instantiated cloud provider from an ``integrations`` row. The inventory sync (``sync.py``), the DNS write path (``dns_request_service.py``), and the admin direct-action router each inline this same decrypt-and-merge dance. This helper centralizes it for new call sites (metrics sync) — same contract: decrypt secrets, merge over non-secret ``config_public``, hand the flat ``fields`` dict to the provider class. """ from __future__ import annotations import uuid from sqlalchemy.ext.asyncio import AsyncSession from app.core.exceptions import AppException from app.domains.cmdb.credential_crypto import decrypt_fields from app.domains.cmdb.providers.base import ( CloudProvider, ProviderCredentials, get_provider_class, ) from app.integrations.models import Integration async def build_cloud_provider( db: AsyncSession, integration_id: str, ) -> tuple[CloudProvider, str]: """Return ``(provider, account_alias)`` for a cloud integration. Raises ``AppException`` with an actionable message for the caller to translate into a skipped / failed job result. The caller owns ``provider.close()``. """ integration = await db.get(Integration, uuid.UUID(str(integration_id))) if integration is None or integration.kind != "cloud": raise AppException("CLOUD_INTEGRATION_NOT_FOUND", 404) # Cache ORM attributes into locals up-front (a later rollback expires # the identity map; re-reading an expired column in async code raises # the greenlet_spawn error — same guard sync.py documents). alias = integration.name or str(integration_id) provider_type = integration.provider secrets_encrypted = integration.secrets_encrypted secrets_nonce = integration.secrets_nonce config_public = integration.config_public or {} if not secrets_encrypted: raise AppException("CLOUD_INTEGRATION_NO_CREDENTIALS", 400) cred_fields = decrypt_fields( secrets_encrypted, secrets_nonce, integration_id=str(integration_id), ) merged: dict[str, str] = {} if config_public: merged.update( {k: str(v) for k, v in config_public.items() if v is not None}, ) merged.update(cred_fields) creds = ProviderCredentials( provider_id=str(integration_id), provider_type=provider_type, account_alias=alias, fields=merged, ) provider = get_provider_class(provider_type)(creds) return provider, alias