test_kestra_runner_execution.py 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196
  1. """Opt-in Kestra -> signed token -> Runner -> governed pool acceptance."""
  2. import os
  3. import time
  4. import pytest
  5. import requests
  6. from neo4j import GraphDatabase
  7. from sqlalchemy import create_engine, text
  8. from app.core.common.identifiers import new_governance_uid
  9. from app.core.data_source.credentials import (
  10. CredentialCodec,
  11. DataSourceCredentialRepository,
  12. )
  13. from app.core.data_source.definitions import DataSourceDefinitionRepository
  14. from app.core.data_source.models import (
  15. DataSourceCredential,
  16. DataSourceDefinition,
  17. )
  18. from app.core.orchestration.compilers import compile_kestra_flow
  19. from app.core.orchestration.engines import KestraAdapter
  20. from app.runner.auth import TaskTokenIssuer
  21. pytestmark = pytest.mark.integration
  22. def test_kestra_passes_one_signed_task_to_the_runner():
  23. if os.environ.get("RUN_RUNNER_INTEGRATION") != "1":
  24. pytest.skip("set RUN_RUNNER_INTEGRATION=1 for Kestra Runner acceptance")
  25. platform_engine = create_engine(
  26. "postgresql://dataops:dataops-test-password@127.0.0.1:15432/dataops"
  27. )
  28. graph_driver = GraphDatabase.driver(
  29. "bolt://127.0.0.1:17687",
  30. auth=("neo4j", "Passw0rd"),
  31. )
  32. source_uid = new_governance_uid()
  33. dataflow_uid = new_governance_uid()
  34. node = {
  35. "id": "read_customers",
  36. "type": "sql.query",
  37. "data_source_uid": source_uid,
  38. "purpose": "read",
  39. "config": {
  40. "statement": (
  41. "SELECT customer_name FROM acceptance_customers "
  42. "WHERE id >= :minimum_id ORDER BY id"
  43. ),
  44. "parameters": {"minimum_id": "${parameters.minimum_id}"},
  45. },
  46. }
  47. task_uid = new_governance_uid()
  48. token = TaskTokenIssuer(
  49. "dataops-local-runner-task-token-secret-change-me",
  50. ttl_seconds=120,
  51. ).issue(
  52. task_uid=task_uid,
  53. dataflow_uid=dataflow_uid,
  54. deployment_id="01900000-0000-7000-8000-000000000099",
  55. environment="test",
  56. workflow_version=1,
  57. correlation_id=new_governance_uid(),
  58. node=node,
  59. )
  60. compiled = compile_kestra_flow(
  61. {
  62. "schema_version": "1.0",
  63. "dataflow_uid": dataflow_uid,
  64. "name": "V52 Kestra Runner acceptance",
  65. "nodes": [node],
  66. "edges": [],
  67. "parameters": {
  68. "minimum_id": {"type": "integer", "required": True}
  69. },
  70. },
  71. {
  72. "schema_version": "1.0",
  73. "timezone": "Asia/Shanghai",
  74. "triggers": [{"type": "manual"}],
  75. "max_concurrency": 1,
  76. "conflict_policy": "skip",
  77. "timeout_seconds": 60,
  78. "retry": {"max_attempts": 1, "delay_seconds": 0},
  79. "backfill": {"max_days": 1, "max_runs": 1},
  80. },
  81. "test",
  82. 1,
  83. )
  84. kestra = KestraAdapter(
  85. base_url="http://127.0.0.1:18080/api/v1",
  86. username="admin@dataops.local",
  87. password="DataOpsKestra1!",
  88. )
  89. flow_url = (
  90. "http://127.0.0.1:18080/api/v1/main/flows/"
  91. f"{compiled.namespace}/{compiled.flow_id}"
  92. )
  93. codec = CredentialCodec.from_base64(
  94. "MDEyMzQ1Njc4OWFiY2RlZjAxMjM0NTY3ODlhYmNkZWY=",
  95. "v1",
  96. )
  97. try:
  98. with platform_engine.begin() as connection:
  99. sealed = DataSourceCredentialRepository(codec).create_version(
  100. connection,
  101. data_source_uid=source_uid,
  102. credential=DataSourceCredential(
  103. "source_reader",
  104. "source-test-password",
  105. ),
  106. actor_uid=None,
  107. )
  108. with graph_driver.session() as session:
  109. DataSourceDefinitionRepository(session).save(
  110. DataSourceDefinition(
  111. uid=source_uid,
  112. name_en="v52-kestra-runner-postgres",
  113. database_type="postgresql",
  114. host="source-postgres",
  115. port=5432,
  116. database="acceptance",
  117. credential_ref=source_uid,
  118. credential_version=sealed.credential_version,
  119. pool_size=1,
  120. max_overflow=1,
  121. )
  122. )
  123. kestra.deploy_disabled(compiled.yaml)
  124. kestra.activate(compiled.namespace, compiled.flow_id)
  125. execution = kestra.execute(
  126. compiled.namespace,
  127. compiled.flow_id,
  128. {
  129. "minimum_id": 1,
  130. "dataops_task_tokens": {"read_customers": token},
  131. },
  132. )
  133. execution_id = execution["id"]
  134. current = ""
  135. for _attempt in range(40):
  136. current = kestra.get_execution(execution_id)["state"]["current"]
  137. if current in {"SUCCESS", "FAILED", "KILLED", "CANCELLED"}:
  138. break
  139. time.sleep(0.25)
  140. assert current == "SUCCESS", kestra.get_logs(execution_id)
  141. with platform_engine.connect() as connection:
  142. ledger = connection.execute(
  143. text(
  144. """
  145. SELECT status, commit_outcome
  146. FROM public.runner_task_executions
  147. WHERE task_uid = CAST(:task_uid AS uuid)
  148. """
  149. ),
  150. {"task_uid": task_uid},
  151. ).mappings().one()
  152. assert dict(ledger) == {
  153. "status": "success",
  154. "commit_outcome": "not_applicable",
  155. }
  156. finally:
  157. requests.delete(
  158. flow_url,
  159. auth=("admin@dataops.local", "DataOpsKestra1!"),
  160. timeout=20,
  161. )
  162. with graph_driver.session() as session:
  163. DataSourceDefinitionRepository(session).delete(source_uid)
  164. with platform_engine.begin() as connection:
  165. connection.execute(
  166. text(
  167. "DELETE FROM public.runner_task_executions "
  168. "WHERE task_uid = CAST(:task_uid AS uuid)"
  169. ),
  170. {"task_uid": task_uid},
  171. )
  172. connection.execute(
  173. text(
  174. "DELETE FROM public.datasource_credential_audit_events "
  175. "WHERE data_source_uid = CAST(:uid AS uuid)"
  176. ),
  177. {"uid": source_uid},
  178. )
  179. connection.execute(
  180. text(
  181. "DELETE FROM public.datasource_credentials "
  182. "WHERE data_source_uid = CAST(:uid AS uuid)"
  183. ),
  184. {"uid": source_uid},
  185. )
  186. graph_driver.close()
  187. platform_engine.dispose()