test_kestra_runner_execution.py 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195
  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. workflow_version=1,
  55. correlation_id=new_governance_uid(),
  56. node=node,
  57. )
  58. compiled = compile_kestra_flow(
  59. {
  60. "schema_version": "1.0",
  61. "dataflow_uid": dataflow_uid,
  62. "name": "V52 Kestra Runner acceptance",
  63. "nodes": [node],
  64. "edges": [],
  65. "parameters": {
  66. "minimum_id": {"type": "integer", "required": True}
  67. },
  68. },
  69. {
  70. "schema_version": "1.0",
  71. "timezone": "Asia/Shanghai",
  72. "triggers": [{"type": "manual"}],
  73. "max_concurrency": 1,
  74. "conflict_policy": "skip",
  75. "timeout_seconds": 60,
  76. "retry": {"max_attempts": 1, "delay_seconds": 0},
  77. "backfill": {"max_days": 1, "max_runs": 1},
  78. },
  79. "test",
  80. 1,
  81. )
  82. kestra = KestraAdapter(
  83. base_url="http://127.0.0.1:18080/api/v1",
  84. username="admin@dataops.local",
  85. password="DataOpsKestra1!",
  86. )
  87. flow_url = (
  88. "http://127.0.0.1:18080/api/v1/main/flows/"
  89. f"{compiled.namespace}/{compiled.flow_id}"
  90. )
  91. codec = CredentialCodec.from_base64(
  92. "MDEyMzQ1Njc4OWFiY2RlZjAxMjM0NTY3ODlhYmNkZWY=",
  93. "v1",
  94. )
  95. try:
  96. with platform_engine.begin() as connection:
  97. sealed = DataSourceCredentialRepository(codec).create_version(
  98. connection,
  99. data_source_uid=source_uid,
  100. credential=DataSourceCredential(
  101. "source_reader",
  102. "source-test-password",
  103. ),
  104. actor_uid=None,
  105. )
  106. with graph_driver.session() as session:
  107. DataSourceDefinitionRepository(session).save(
  108. DataSourceDefinition(
  109. uid=source_uid,
  110. name_en="v52-kestra-runner-postgres",
  111. database_type="postgresql",
  112. host="source-postgres",
  113. port=5432,
  114. database="acceptance",
  115. credential_ref=source_uid,
  116. credential_version=sealed.credential_version,
  117. pool_size=1,
  118. max_overflow=1,
  119. )
  120. )
  121. kestra.deploy_disabled(compiled.yaml)
  122. kestra.activate(compiled.namespace, compiled.flow_id)
  123. execution = kestra.execute(
  124. compiled.namespace,
  125. compiled.flow_id,
  126. {
  127. "minimum_id": 1,
  128. "dataops_task_tokens": {"read_customers": token},
  129. },
  130. )
  131. execution_id = execution["id"]
  132. current = ""
  133. for _attempt in range(40):
  134. current = kestra.get_execution(execution_id)["state"]["current"]
  135. if current in {"SUCCESS", "FAILED", "KILLED", "CANCELLED"}:
  136. break
  137. time.sleep(0.25)
  138. assert current == "SUCCESS", kestra.get_logs(execution_id)
  139. with platform_engine.connect() as connection:
  140. ledger = connection.execute(
  141. text(
  142. """
  143. SELECT status, commit_outcome
  144. FROM public.runner_task_executions
  145. WHERE task_uid = CAST(:task_uid AS uuid)
  146. """
  147. ),
  148. {"task_uid": task_uid},
  149. ).mappings().one()
  150. assert dict(ledger) == {
  151. "status": "success",
  152. "commit_outcome": "not_applicable",
  153. }
  154. finally:
  155. requests.delete(
  156. flow_url,
  157. auth=("admin@dataops.local", "DataOpsKestra1!"),
  158. timeout=20,
  159. )
  160. with graph_driver.session() as session:
  161. DataSourceDefinitionRepository(session).delete(source_uid)
  162. with platform_engine.begin() as connection:
  163. connection.execute(
  164. text(
  165. "DELETE FROM public.runner_task_executions "
  166. "WHERE task_uid = CAST(:task_uid AS uuid)"
  167. ),
  168. {"task_uid": task_uid},
  169. )
  170. connection.execute(
  171. text(
  172. "DELETE FROM public.datasource_credential_audit_events "
  173. "WHERE data_source_uid = CAST(:uid AS uuid)"
  174. ),
  175. {"uid": source_uid},
  176. )
  177. connection.execute(
  178. text(
  179. "DELETE FROM public.datasource_credentials "
  180. "WHERE data_source_uid = CAST(:uid AS uuid)"
  181. ),
  182. {"uid": source_uid},
  183. )
  184. graph_driver.close()
  185. platform_engine.dispose()