bootstrap.py 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133
  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 app.core.data_source.runtime import (
  6. DataSourceRuntimeConfig,
  7. build_standalone_data_source_runtime,
  8. )
  9. from app.runner.api import create_runner_app
  10. from app.runner.auth import TaskTokenVerifier
  11. from app.runner.ledger import PostgresTaskLedger
  12. from app.runner.nodes import (
  13. GovernedHttpExecutor,
  14. NodeRegistry,
  15. RestrictedPythonExecutor,
  16. SqlExecuteExecutor,
  17. SqlQueryExecutor,
  18. )
  19. def _required(name):
  20. value = str(os.environ.get(name, "")).strip()
  21. if not value:
  22. raise ValueError(f"{name} is required")
  23. return value
  24. def _integer(name, default, minimum, maximum):
  25. try:
  26. value = int(os.environ.get(name, str(default)))
  27. except (TypeError, ValueError) as exc:
  28. raise ValueError(f"{name} must be an integer") from exc
  29. if value < minimum or value > maximum:
  30. raise ValueError(f"{name} must be between {minimum} and {maximum}")
  31. return value
  32. @dataclass(frozen=True)
  33. class RunnerSettings:
  34. runtime: DataSourceRuntimeConfig = field(repr=False)
  35. task_token_secret: str = field(repr=False)
  36. allowed_http_hosts: frozenset = field(default_factory=frozenset)
  37. task_token_ttl_seconds: int = 60
  38. max_query_rows: int = 1000
  39. def runner_settings_from_env():
  40. task_token_secret = _required("RUNNER_TASK_TOKEN_SECRET")
  41. worker_count = _integer("RUNNER_WORKERS", 2, 1, 8)
  42. runtime = DataSourceRuntimeConfig.from_mapping(
  43. {
  44. "platform_database_url": _required("DATABASE_URL"),
  45. "neo4j_uri": _required("NEO4J_URI"),
  46. "neo4j_user": _required("NEO4J_USER"),
  47. "neo4j_password": _required("NEO4J_PASSWORD"),
  48. "credential_master_key": _required(
  49. "DATASOURCE_CREDENTIAL_MASTER_KEY"
  50. ),
  51. "credential_key_version": _required(
  52. "DATASOURCE_CREDENTIAL_KEY_VERSION"
  53. ),
  54. "certificate_dir": os.environ.get(
  55. "DATASOURCE_CERT_DIR",
  56. "/etc/dataops-platform/datasource-certs",
  57. ),
  58. "pool_size": _integer("RUNNER_DATASOURCE_POOL_SIZE", 1, 1, 3),
  59. "max_overflow": _integer(
  60. "RUNNER_DATASOURCE_MAX_OVERFLOW", 1, 0, 3
  61. ),
  62. "pool_timeout": _integer(
  63. "RUNNER_DATASOURCE_POOL_TIMEOUT", 10, 1, 60
  64. ),
  65. "pool_recycle": _integer(
  66. "RUNNER_DATASOURCE_POOL_RECYCLE", 1800, 60, 86400
  67. ),
  68. "idle_ttl": _integer(
  69. "RUNNER_DATASOURCE_POOL_IDLE_TTL", 900, 60, 86400
  70. ),
  71. "max_idle_pools": _integer(
  72. "RUNNER_DATASOURCE_MAX_IDLE_POOLS", 4, 1, 20
  73. ),
  74. "query_timeout": _integer(
  75. "RUNNER_DATASOURCE_QUERY_TIMEOUT", 30, 1, 300
  76. ),
  77. "worker_count": worker_count,
  78. "connection_budget": _integer(
  79. "RUNNER_DATASOURCE_CONNECTION_BUDGET", 32, 1, 200
  80. ),
  81. }
  82. )
  83. allowed_hosts = frozenset(
  84. host.strip().lower()
  85. for host in os.environ.get("RUNNER_HTTP_ALLOWED_HOSTS", "").split(",")
  86. if host.strip()
  87. )
  88. return RunnerSettings(
  89. runtime=runtime,
  90. task_token_secret=task_token_secret,
  91. allowed_http_hosts=allowed_hosts,
  92. task_token_ttl_seconds=_integer(
  93. "RUNNER_TASK_TOKEN_TTL_SECONDS", 60, 1, 300
  94. ),
  95. max_query_rows=_integer("RUNNER_MAX_QUERY_ROWS", 1000, 1, 10000),
  96. )
  97. def build_runner_application(settings=None):
  98. settings = settings or runner_settings_from_env()
  99. runtime = build_standalone_data_source_runtime(settings.runtime)
  100. registry = NodeRegistry(
  101. {
  102. "sql.query": SqlQueryExecutor(
  103. runtime.manager,
  104. max_rows=settings.max_query_rows,
  105. ),
  106. "sql.execute": SqlExecuteExecutor(runtime.manager),
  107. "python": RestrictedPythonExecutor({}),
  108. "http": GovernedHttpExecutor(
  109. allowed_hosts=settings.allowed_http_hosts
  110. ),
  111. }
  112. )
  113. application = create_runner_app(
  114. verifier=TaskTokenVerifier(settings.task_token_secret),
  115. ledger=PostgresTaskLedger(runtime.platform_engine),
  116. registry=registry,
  117. )
  118. application.extensions["dataops_runner_runtime"] = runtime
  119. application.extensions["dataops_runner_settings"] = settings
  120. return application