routes.py 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227
  1. """HTTP boundary for secret-free external data-source management."""
  2. import logging
  3. from flask import g, jsonify, request
  4. from app.api.data_source import bp
  5. from app.core.data_source.errors import DataSourceError
  6. from app.core.data_source.redaction import (
  7. redact_mapping,
  8. sanitize_exception,
  9. )
  10. from app.models.result import failed, success
  11. logger = logging.getLogger(__name__)
  12. def get_data_source_service():
  13. from app.core.data_source.runtime import get_data_source_manager
  14. from app.core.data_source.service import build_data_source_service
  15. return build_data_source_service(get_data_source_manager())
  16. def _actor_uid():
  17. identity = getattr(g, "current_user", {}) or {}
  18. return identity.get("id") or identity.get("sub")
  19. def _error_response(error):
  20. if isinstance(error, DataSourceError):
  21. logger.warning(
  22. "数据源操作失败: code=%s message=%s",
  23. error.code,
  24. sanitize_exception(error),
  25. )
  26. return (
  27. jsonify(
  28. failed(
  29. str(error),
  30. code=error.http_status,
  31. error={"code": error.code},
  32. )
  33. ),
  34. error.http_status,
  35. )
  36. logger.error(
  37. "数据源操作异常: %s",
  38. sanitize_exception(error),
  39. )
  40. return (
  41. jsonify(
  42. failed(
  43. "数据源操作失败",
  44. code=500,
  45. error={"code": "DATASOURCE_ERROR"},
  46. )
  47. ),
  48. 500,
  49. )
  50. @bp.route("/save", methods=["POST"])
  51. def data_source_save():
  52. payload = request.get_json(silent=True) or {}
  53. logger.debug("保存数据源请求: %s", redact_mapping(payload))
  54. try:
  55. service = get_data_source_service()
  56. definition, created = service.save(
  57. payload,
  58. actor_uid=_actor_uid(),
  59. )
  60. status = 201 if created else 200
  61. return jsonify(success(service.serialize(definition))), status
  62. except Exception as error:
  63. return _error_response(error)
  64. @bp.route("/list", methods=["POST"])
  65. def data_source_list():
  66. payload = request.get_json(silent=True) or {}
  67. try:
  68. service = get_data_source_service()
  69. definitions = service.list(payload)
  70. items = [service.serialize(item) for item in definitions]
  71. return jsonify(
  72. success({"data_source": items, "total": len(items)})
  73. ), 200
  74. except Exception as error:
  75. return _error_response(error)
  76. @bp.route("/delete", methods=["POST"])
  77. def data_source_delete():
  78. payload = request.get_json(silent=True) or {}
  79. logger.debug("删除数据源请求: %s", redact_mapping(payload))
  80. try:
  81. result = get_data_source_service().delete(
  82. payload.get("uid"),
  83. actor_uid=_actor_uid(),
  84. )
  85. return jsonify(success(result)), 200
  86. except Exception as error:
  87. return _error_response(error)
  88. @bp.route("/conntest", methods=["POST"])
  89. def data_source_conn_test():
  90. payload = request.get_json(silent=True) or {}
  91. logger.debug("测试数据源连接请求: %s", redact_mapping(payload))
  92. try:
  93. result = get_data_source_service().test_connection(payload)
  94. return jsonify(success(result)), 200
  95. except Exception as error:
  96. return _error_response(error)
  97. @bp.route("/valid", methods=["POST"])
  98. def data_source_connstr_valid():
  99. payload = request.get_json(silent=True) or {}
  100. logger.debug("验证数据源连接请求: %s", redact_mapping(payload))
  101. try:
  102. result = get_data_source_service().test_connection(payload)
  103. return jsonify(success({"exists": False, **result})), 200
  104. except Exception as error:
  105. return _error_response(error)
  106. @bp.route("/parse", methods=["POST"])
  107. def data_source_connstr_parse():
  108. return (
  109. jsonify(
  110. failed(
  111. "连接字符串快捷解析已停用",
  112. code=410,
  113. error={"code": "DATASOURCE_PARSE_RETIRED"},
  114. )
  115. ),
  116. 410,
  117. )
  118. @bp.route("/pools", methods=["GET"])
  119. def data_source_pool_list():
  120. try:
  121. service = get_data_source_service()
  122. items = [
  123. service.serialize_pool_status(status)
  124. for status in service.pool_statuses()
  125. ]
  126. return jsonify(success({"pools": items, "total": len(items)})), 200
  127. except Exception as error:
  128. return _error_response(error)
  129. @bp.route("/<data_source_uid>/pool", methods=["GET"])
  130. def data_source_pool_status(data_source_uid):
  131. try:
  132. service = get_data_source_service()
  133. status = service.pool_status(data_source_uid)
  134. serializer = getattr(
  135. service,
  136. "serialize_pool_status",
  137. None,
  138. )
  139. data = (
  140. serializer(status)
  141. if serializer is not None
  142. else {
  143. "data_source_uid": status.data_source_uid,
  144. "credential_version": status.credential_version,
  145. "pool_state": status.pool_state,
  146. "pool_size": status.pool_size,
  147. "checked_out": status.checked_out,
  148. "checked_in": status.checked_in,
  149. "overflow": status.overflow,
  150. "leases": status.leases,
  151. }
  152. )
  153. return jsonify(success(data)), 200
  154. except Exception as error:
  155. return _error_response(error)
  156. @bp.route("/<data_source_uid>/pool/invalidate", methods=["POST"])
  157. def data_source_pool_invalidate(data_source_uid):
  158. payload = request.get_json(silent=True) or {}
  159. reason = payload.get("reason")
  160. if reason not in {
  161. "admin_reset",
  162. "configuration_changed",
  163. "credential_rotated",
  164. }:
  165. return (
  166. jsonify(
  167. failed(
  168. "连接池失效原因无效",
  169. code=400,
  170. error={"code": "DATASOURCE_CONFIGURATION_INVALID"},
  171. )
  172. ),
  173. 400,
  174. )
  175. try:
  176. result = get_data_source_service().invalidate_pool(
  177. data_source_uid,
  178. reason=reason,
  179. actor_uid=_actor_uid(),
  180. )
  181. return jsonify(success(result)), 200
  182. except Exception as error:
  183. return _error_response(error)
  184. @bp.route("/graph", methods=["POST"])
  185. def data_source_graph_relationship():
  186. return (
  187. jsonify(
  188. failed(
  189. "该功能尚未实现",
  190. code=501,
  191. error={"code": "DATASOURCE_GRAPH_NOT_IMPLEMENTED"},
  192. )
  193. ),
  194. 501,
  195. )