bootstrap.py 8.2 KB

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