test_kestra_local_contract.py 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155
  1. import json
  2. import subprocess
  3. from pathlib import Path
  4. ROOT = Path(__file__).resolve().parents[1]
  5. COMPOSE = ROOT / "deploy/docker/docker-compose.yml"
  6. KESTRA_CONFIG = ROOT / "deploy/docker/kestra/application.yml"
  7. POSTGRES_BOOTSTRAP = ROOT / "deploy/docker/postgres/init/000-init.sql"
  8. KESTRA_BOOTSTRAP = (
  9. ROOT / "deploy/docker/postgres/init/010-kestra-runtime.sql"
  10. )
  11. KESTRA_SOAK = ROOT / "scripts/validation/kestra_queue_soak.sh"
  12. def _compose_config():
  13. result = subprocess.run(
  14. [
  15. "docker",
  16. "compose",
  17. "-f",
  18. str(COMPOSE),
  19. "--profile",
  20. "kestra-mcp",
  21. "config",
  22. "--format",
  23. "json",
  24. ],
  25. cwd=ROOT,
  26. check=True,
  27. capture_output=True,
  28. text=True,
  29. )
  30. return json.loads(result.stdout)
  31. def test_kestra_and_mcp_images_are_immutable_and_network_is_isolated():
  32. config = _compose_config()
  33. kestra = config["services"]["kestra"]
  34. mcp = config["services"]["kestra-mcp"]
  35. assert kestra["image"] == "kestra/kestra:v1.3.20"
  36. assert "--worker-thread=16" in kestra["command"]
  37. assert mcp["image"] == (
  38. "ghcr.io/kestra-io/mcp-server-python@"
  39. "sha256:1fd62028f60914af31e244ddb0086ed5017420313e2c8547ea9c01b8d24dfc1d"
  40. )
  41. assert kestra["ports"] == [
  42. {
  43. "mode": "ingress",
  44. "target": 8080,
  45. "published": "18080",
  46. "host_ip": "127.0.0.1",
  47. "protocol": "tcp",
  48. }
  49. ]
  50. assert "ports" not in mcp
  51. assert set(kestra["networks"]) == {"dataops-test-net"}
  52. assert set(mcp["networks"]) == {"dataops-test-net"}
  53. assert mcp["profiles"] == ["kestra-mcp"]
  54. assert mcp["stdin_open"] is True
  55. def test_kestra_uses_its_own_database_and_mcp_has_a_restricted_tool_surface():
  56. config = _compose_config()
  57. kestra_environment = config["services"]["kestra"]["environment"]
  58. mcp_environment = config["services"]["kestra-mcp"]["environment"]
  59. assert kestra_environment["KESTRA_DB_NAME"] == "kestra"
  60. assert kestra_environment["KESTRA_DB_HOST"] == "postgres"
  61. assert kestra_environment["KESTRA_DB_USERNAME"] == "kestra_runtime"
  62. assert config["services"]["kestra"]["restart"] == "unless-stopped"
  63. assert (
  64. config["services"]["kestra"]["depends_on"]["kestra-db-init"][
  65. "condition"
  66. ]
  67. == "service_completed_successfully"
  68. )
  69. assert mcp_environment["KESTRA_BASE_URL"] == "http://kestra:8080/api/v1"
  70. assert mcp_environment["KESTRA_TENANT_ID"] == "main"
  71. assert mcp_environment["KESTRA_MCP_LOG_LEVEL"] == "ERROR"
  72. assert set(mcp_environment["KESTRA_MCP_DISABLED_TOOLS"].split(",")) == {
  73. "ee",
  74. "files",
  75. "kv",
  76. "namespace",
  77. }
  78. combined = json.dumps(
  79. {
  80. "kestra": config["services"]["kestra"],
  81. "kestra-mcp": config["services"]["kestra-mcp"],
  82. },
  83. sort_keys=True,
  84. )
  85. for forbidden in (
  86. "source-postgres",
  87. "source-mysql",
  88. "source-test-password",
  89. "source-root-test-password",
  90. "DATASOURCE_CREDENTIAL_MASTER_KEY",
  91. ):
  92. assert forbidden not in combined
  93. def test_kestra_configuration_and_fresh_database_bootstrap_are_present():
  94. assert KESTRA_CONFIG.is_file()
  95. source = KESTRA_CONFIG.read_text(encoding="utf-8")
  96. assert "datasources:" in source
  97. assert "postgres:" in source
  98. assert (
  99. "url: jdbc:postgresql://${KESTRA_DB_HOST}:5432/"
  100. "${KESTRA_DB_NAME}?tcpKeepAlive=true"
  101. ) in source
  102. assert "maximum-pool-size: 20" in source
  103. assert "minimum-idle: 4" in source
  104. assert "connection-test-query: SELECT 1" in source
  105. assert "basic-auth:" in source
  106. assert "${KESTRA_USERNAME}" in source
  107. assert "${KESTRA_PASSWORD}" in source
  108. assert "type: local" in source
  109. assert "tutorial-flows:" in source
  110. assert "enabled: false" in source
  111. bootstrap = POSTGRES_BOOTSTRAP.read_text(encoding="utf-8")
  112. assert "CREATE DATABASE kestra" in bootstrap
  113. dedicated = KESTRA_BOOTSTRAP.read_text(encoding="utf-8")
  114. assert "CREATE ROLE kestra_runtime LOGIN" in dedicated
  115. assert "ALTER DATABASE kestra OWNER TO kestra_runtime" in dedicated
  116. assert "pg_catalog" in dedicated
  117. soak = KESTRA_SOAK.read_text(encoding="utf-8")
  118. assert "KESTRA_SOAK_SECONDS:-1800" in soak
  119. assert "executions/${namespace}/${flow_id}" in soak
  120. assert "dataops-kestra-soak-" in soak
  121. assert 'restart postgres' in soak
  122. assert 'restart kestra' in soak
  123. assert "controlled_restart_completed" in soak
  124. assert 'kind:"soak_start"' in soak
  125. assert 'kind:"soak_complete"' in soak
  126. assert 'logs --since "$soak_started_at"' in soak
  127. assert "postgres_restart_count_before" in soak
  128. assert "kestra_restart_count_before" in soak
  129. assert "Unexpected automatic container restart" in soak
  130. assert "explicit `docker restart` does not consume RestartCount" in soak
  131. assert "broken pipe" in soak
  132. assert "terminating connection due to administrator command" in soak
  133. assert "every other FATAL line" in soak
  134. def test_kestra_mcp_is_documented_as_stdio_not_an_http_endpoint():
  135. readme = (ROOT / "deploy/docker/README.md").read_text(encoding="utf-8")
  136. assert "http://127.0.0.1:18080" in readme
  137. assert "Kestra MCP" in readme
  138. assert "stdio" in readme
  139. assert "docker compose --profile kestra-mcp run --rm -T kestra-mcp" in readme