bootstrap.py 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223
  1. """Environment parsing and dependency wiring for the standalone Runner."""
  2. from __future__ import annotations
  3. import os
  4. from dataclasses import dataclass, field
  5. from minio import Minio
  6. from app.core.data_source.runtime import (
  7. DataSourceRuntimeConfig,
  8. build_standalone_data_source_runtime,
  9. )
  10. from app.runner.api import create_runner_app
  11. from app.runner.artifacts import ArtifactStore, PostgresArtifactResolver
  12. from app.runner.auth import TaskTokenVerifier
  13. from app.runner.ledger import PostgresTaskLedger
  14. from app.runner.nodes import (
  15. GovernedHttpExecutor,
  16. NodeRegistry,
  17. RestrictedPythonExecutor,
  18. SqlExecuteExecutor,
  19. SqlQueryExecutor,
  20. )
  21. from app.runner.rule_polars import PolarsRulePlanAdapter
  22. from app.runner.rule_sql import (
  23. SqlGlotQualityPlanAdapter,
  24. SqlGlotRulePlanAdapter,
  25. )
  26. from app.runner.rules import (
  27. PostgresRulePlanRepository,
  28. RulePlanExecutor,
  29. )
  30. def _required(name):
  31. value = str(os.environ.get(name, "")).strip()
  32. if not value:
  33. raise ValueError(f"{name} is required")
  34. return value
  35. def _integer(name, default, minimum, maximum):
  36. try:
  37. value = int(os.environ.get(name, str(default)))
  38. except (TypeError, ValueError) as exc:
  39. raise ValueError(f"{name} must be an integer") from exc
  40. if value < minimum or value > maximum:
  41. raise ValueError(f"{name} must be between {minimum} and {maximum}")
  42. return value
  43. def _boolean(name, default=False):
  44. value = str(os.environ.get(name, str(default))).strip().lower()
  45. if value in {"1", "true", "yes", "on"}:
  46. return True
  47. if value in {"0", "false", "no", "off"}:
  48. return False
  49. raise ValueError(f"{name} must be a boolean")
  50. @dataclass(frozen=True)
  51. class RunnerSettings:
  52. runtime: DataSourceRuntimeConfig = field(repr=False)
  53. task_token_secret: str = field(repr=False)
  54. artifact_host: str = field(repr=False)
  55. artifact_user: str = field(repr=False)
  56. artifact_password: str = field(repr=False)
  57. artifact_bucket: str = "dataops-rules"
  58. artifact_secure: bool = False
  59. artifact_max_bytes: int = 32 * 1024 * 1024
  60. artifact_max_rows: int = 100_000
  61. artifact_memory_limit_bytes: int = 256 * 1024 * 1024
  62. artifact_ttl_seconds: int = 3600
  63. allowed_http_hosts: frozenset = field(default_factory=frozenset)
  64. task_token_ttl_seconds: int = 60
  65. max_query_rows: int = 1000
  66. def runner_settings_from_env():
  67. task_token_secret = _required("RUNNER_TASK_TOKEN_SECRET")
  68. worker_count = _integer("RUNNER_WORKERS", 2, 1, 8)
  69. runtime = DataSourceRuntimeConfig.from_mapping(
  70. {
  71. "platform_database_url": _required("DATABASE_URL"),
  72. "neo4j_uri": _required("NEO4J_URI"),
  73. "neo4j_user": _required("NEO4J_USER"),
  74. "neo4j_password": _required("NEO4J_PASSWORD"),
  75. "credential_master_key": _required(
  76. "DATASOURCE_CREDENTIAL_MASTER_KEY"
  77. ),
  78. "credential_key_version": _required(
  79. "DATASOURCE_CREDENTIAL_KEY_VERSION"
  80. ),
  81. "certificate_dir": os.environ.get(
  82. "DATASOURCE_CERT_DIR",
  83. "/etc/dataops-platform/datasource-certs",
  84. ),
  85. "pool_size": _integer("RUNNER_DATASOURCE_POOL_SIZE", 1, 1, 3),
  86. "max_overflow": _integer(
  87. "RUNNER_DATASOURCE_MAX_OVERFLOW", 1, 0, 3
  88. ),
  89. "pool_timeout": _integer(
  90. "RUNNER_DATASOURCE_POOL_TIMEOUT", 10, 1, 60
  91. ),
  92. "pool_recycle": _integer(
  93. "RUNNER_DATASOURCE_POOL_RECYCLE", 1800, 60, 86400
  94. ),
  95. "idle_ttl": _integer(
  96. "RUNNER_DATASOURCE_POOL_IDLE_TTL", 900, 60, 86400
  97. ),
  98. "max_idle_pools": _integer(
  99. "RUNNER_DATASOURCE_MAX_IDLE_POOLS", 4, 1, 20
  100. ),
  101. "query_timeout": _integer(
  102. "RUNNER_DATASOURCE_QUERY_TIMEOUT", 30, 1, 300
  103. ),
  104. "worker_count": worker_count,
  105. "connection_budget": _integer(
  106. "RUNNER_DATASOURCE_CONNECTION_BUDGET", 32, 1, 200
  107. ),
  108. }
  109. )
  110. allowed_hosts = frozenset(
  111. host.strip().lower()
  112. for host in os.environ.get("RUNNER_HTTP_ALLOWED_HOSTS", "").split(",")
  113. if host.strip()
  114. )
  115. return RunnerSettings(
  116. runtime=runtime,
  117. task_token_secret=task_token_secret,
  118. artifact_host=_required("RUNNER_MINIO_HOST"),
  119. artifact_user=_required("RUNNER_MINIO_USER"),
  120. artifact_password=_required("RUNNER_MINIO_PASSWORD"),
  121. artifact_bucket=_required("RUNNER_MINIO_BUCKET"),
  122. artifact_secure=_boolean("RUNNER_MINIO_SECURE", False),
  123. artifact_max_bytes=_integer(
  124. "RUNNER_ARTIFACT_MAX_BYTES",
  125. 32 * 1024 * 1024,
  126. 1024,
  127. 2 * 1024 * 1024 * 1024,
  128. ),
  129. artifact_max_rows=_integer(
  130. "RUNNER_ARTIFACT_MAX_ROWS", 100_000, 1, 10_000_000
  131. ),
  132. artifact_memory_limit_bytes=_integer(
  133. "RUNNER_ARTIFACT_MEMORY_LIMIT_BYTES",
  134. 256 * 1024 * 1024,
  135. 16 * 1024 * 1024,
  136. 16 * 1024 * 1024 * 1024,
  137. ),
  138. artifact_ttl_seconds=_integer(
  139. "RUNNER_ARTIFACT_TTL_SECONDS", 3600, 1, 86400
  140. ),
  141. allowed_http_hosts=allowed_hosts,
  142. task_token_ttl_seconds=_integer(
  143. "RUNNER_TASK_TOKEN_TTL_SECONDS", 60, 1, 300
  144. ),
  145. max_query_rows=_integer("RUNNER_MAX_QUERY_ROWS", 1000, 1, 10000),
  146. )
  147. def build_runner_application(settings=None):
  148. settings = settings or runner_settings_from_env()
  149. runtime = build_standalone_data_source_runtime(settings.runtime)
  150. query_executor = SqlQueryExecutor(
  151. runtime.manager,
  152. max_rows=settings.max_query_rows,
  153. )
  154. write_executor = SqlExecuteExecutor(runtime.manager)
  155. sql_rule_adapter = SqlGlotRulePlanAdapter(runtime.manager)
  156. artifact_store = ArtifactStore(
  157. Minio(
  158. settings.artifact_host,
  159. access_key=settings.artifact_user,
  160. secret_key=settings.artifact_password,
  161. secure=settings.artifact_secure,
  162. ),
  163. bucket=settings.artifact_bucket,
  164. max_artifact_bytes=settings.artifact_max_bytes,
  165. max_rows=settings.artifact_max_rows,
  166. memory_limit_bytes=settings.artifact_memory_limit_bytes,
  167. max_ttl_seconds=settings.artifact_ttl_seconds,
  168. )
  169. polars_rule_adapter = PolarsRulePlanAdapter(
  170. artifact_store=artifact_store,
  171. artifact_resolver=PostgresArtifactResolver(
  172. runtime.platform_engine, artifact_store
  173. ),
  174. masking_policies={
  175. "customer_mobile_last4": "preserve_last_4",
  176. "redact": "redact",
  177. },
  178. artifact_ttl_seconds=settings.artifact_ttl_seconds,
  179. )
  180. rule_executor = RulePlanExecutor(
  181. PostgresRulePlanRepository(runtime.platform_engine),
  182. adapters={
  183. "sql_pushdown": sql_rule_adapter,
  184. "polars_batch": polars_rule_adapter,
  185. "quality_check": SqlGlotQualityPlanAdapter(),
  186. },
  187. )
  188. registry = NodeRegistry(
  189. {
  190. "sql.query": query_executor,
  191. "sql.execute": write_executor,
  192. "python": RestrictedPythonExecutor({}),
  193. "http": GovernedHttpExecutor(
  194. allowed_hosts=settings.allowed_http_hosts
  195. ),
  196. "rule.apply": rule_executor,
  197. "quality.check": rule_executor,
  198. }
  199. )
  200. application = create_runner_app(
  201. verifier=TaskTokenVerifier(settings.task_token_secret),
  202. ledger=PostgresTaskLedger(runtime.platform_engine),
  203. registry=registry,
  204. )
  205. application.extensions["dataops_runner_runtime"] = runtime
  206. application.extensions["dataops_runner_settings"] = settings
  207. return application