134 lines
5.1 KiB
Python
134 lines
5.1 KiB
Python
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
|