from __future__ import annotations from fastapi import APIRouter, Depends from pydantic import BaseModel from sqlmodel import Session, select # Kept imported here for compatibility with tests and callers that patch the # route module's Portainer connector class. from ..connectors.portainer import PortainerConnector from ..core.redaction import redact_message from ..db.models import Connector from ..db.session import get_session from ..services.connector_health import refresh_connector_statuses, test_and_persist_connector from ..services.inventory_discovery import ( bind_connector_to_discovered, list_connector_bindings, list_connector_candidates, unbind_connector_from_app, ) router = APIRouter(prefix='/api/connectors') class ConnectorBindRequest(BaseModel): discoveredContainerId: str class ConnectorUnbindRequest(BaseModel): appId: str def _serialize_connector(connector: Connector) -> dict: return { 'id': connector.id, 'name': connector.name, 'status': connector.status, 'lastTest': connector.last_test, 'capabilities': connector.capabilities, 'type': connector.type, 'endpoint': None, 'secretRef': connector.secret_ref, } def _required_env_vars(connector_id: str) -> list[str]: mapping = { 'portainer': ['PORTAINER_URL', 'PORTAINER_API_KEY', 'PORTAINER_ENDPOINT_ID'], 'sonarr': ['SONARR_URL', 'SONARR_API_KEY'], 'radarr': ['RADARR_URL', 'RADARR_API_KEY'], 'prowlarr': ['PROWLARR_URL', 'PROWLARR_API_KEY'], 'deluge': ['DELUGE_URL', 'DELUGE_PASSWORD'], 'plex': ['PLEX_URL', 'PLEX_TOKEN'], 'tautulli': ['TAUTULLI_URL', 'TAUTULLI_API_KEY'], 'nginx_proxy_manager': ['NPM_URL', 'NPM_IDENTITY', 'NPM_SECRET'], 'authentik': ['AUTHENTIK_URL', 'AUTHENTIK_TOKEN'], 'widefrog': ['WIDEFROG_URL'], 'unraid': ['UNRAID_URL', 'UNRAID_API_KEY'], 'home_assistant': ['HOME_ASSISTANT_URL', 'HOME_ASSISTANT_TOKEN'], } return mapping.get(connector_id, []) @router.get('') def list_connectors(refresh: bool = False, session: Session = Depends(get_session)): if refresh: refresh_connector_statuses(session) connectors = session.exec(select(Connector).order_by(Connector.id)).all() return [_serialize_connector(connector) for connector in connectors] @router.get('/{connector_id}') def connector_status(connector_id: str, session: Session = Depends(get_session)): connector = session.get(Connector, connector_id) if not connector: return {'success': False, 'message': 'connector_not_found', 'id': connector_id} return _serialize_connector(connector) @router.get('/{connector_id}/candidates') def connector_candidates(connector_id: str, session: Session = Depends(get_session)): connector = session.get(Connector, connector_id) if not connector: return {'success': False, 'message': 'connector_not_found', 'id': connector_id} return list_connector_candidates(session, connector_id) @router.get('/{connector_id}/bindings') def connector_bindings(connector_id: str, session: Session = Depends(get_session)): connector = session.get(Connector, connector_id) if not connector: return {'success': False, 'message': 'connector_not_found', 'id': connector_id} return list_connector_bindings(session, connector_id) @router.post('/{connector_id}/bind') def bind_connector(connector_id: str, payload: ConnectorBindRequest, session: Session = Depends(get_session)): connector = session.get(Connector, connector_id) if not connector: return {'success': False, 'message': 'connector_not_found', 'id': connector_id} return bind_connector_to_discovered(session, connector_id, payload.discoveredContainerId) @router.post('/{connector_id}/unbind') def unbind_connector(connector_id: str, payload: ConnectorUnbindRequest, session: Session = Depends(get_session)): connector = session.get(Connector, connector_id) if not connector: return {'success': False, 'message': 'connector_not_found', 'id': connector_id} return unbind_connector_from_app(session, connector_id, payload.appId) @router.post('/{connector_id}/test') def test_connector(connector_id: str, session: Session = Depends(get_session)): connector = session.get(Connector, connector_id) if not connector: return {'success': False, 'status': 'disconnected', 'message': f'Connector {connector_id} not found'} result = test_and_persist_connector(session, connector_id) status = str(result.get('status', 'disconnected')) reason = result.get('reason') success = status == 'connected' payload: dict[str, object] = { 'success': success, 'status': status, 'id': connector_id, 'message': redact_message(str(reason)) if reason else ('Connector test succeeded.' if success else 'Connector test failed.'), 'missing_config': status == 'missing_config', 'required_env_vars': _required_env_vars(connector_id), 'secretRef': connector.secret_ref, } if reason: payload['reason'] = redact_message(str(reason)) return payload