healthcheck.py 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268
  1. import json
  2. from typing import Any, TYPE_CHECKING
  3. if TYPE_CHECKING:
  4. from render import Render
  5. try:
  6. from .error import RenderError
  7. from .formatter import escape_dollar
  8. from .validations import valid_http_path_or_raise
  9. except ImportError:
  10. from error import RenderError
  11. from formatter import escape_dollar
  12. from validations import valid_http_path_or_raise
  13. class Healthcheck:
  14. def __init__(self, render_instance: "Render"):
  15. self._render_instance = render_instance
  16. self._test: str | list[str] = ""
  17. self._interval_sec: int = 30
  18. self._timeout_sec: int = 5
  19. self._retries: int = 5
  20. self._start_period_sec: int = 15
  21. self._start_interval_sec: int = 2
  22. self._disabled: bool = False
  23. self._use_built_in: bool = False
  24. def _get_test(self):
  25. if isinstance(self._test, str):
  26. return escape_dollar(self._test)
  27. return [escape_dollar(t) for t in self._test]
  28. def disable(self):
  29. self._disabled = True
  30. def use_built_in(self):
  31. self._use_built_in = True
  32. def set_custom_test(self, test: str | list[str]):
  33. if isinstance(test, list):
  34. if test[0] == "CMD" and any(t.startswith("$") for t in test):
  35. raise RenderError(f"Healthcheck with 'CMD' cannot contain shell variables '{test}'")
  36. if self._disabled:
  37. raise RenderError("Cannot set custom test when healthcheck is disabled")
  38. self._test = test
  39. def set_test(self, variant: str, config: dict | None = None):
  40. config = config or {}
  41. self.set_custom_test(test_mapping(variant, config))
  42. def set_interval(self, interval: int):
  43. self._interval_sec = interval
  44. def set_timeout(self, timeout: int):
  45. self._timeout_sec = timeout
  46. def set_retries(self, retries: int):
  47. self._retries = retries
  48. def set_start_period(self, start_period: int):
  49. self._start_period_sec = start_period
  50. def set_start_interval(self, start_interval: int):
  51. self._start_interval_sec = start_interval
  52. def has_healthcheck(self):
  53. return not self._use_built_in
  54. def render(self):
  55. if self._use_built_in:
  56. return RenderError("Should not be called when built in healthcheck is used")
  57. if self._disabled:
  58. return {"disable": True}
  59. if not self._test:
  60. raise RenderError("Healthcheck test is not set")
  61. return {
  62. "test": self._get_test(),
  63. "retries": self._retries,
  64. "interval": f"{self._interval_sec}s",
  65. "timeout": f"{self._timeout_sec}s",
  66. "start_period": f"{self._start_period_sec}s",
  67. "start_interval": f"{self._start_interval_sec}s",
  68. }
  69. def test_mapping(variant: str, config: dict | None = None) -> list[str]:
  70. config = config or {}
  71. tests = {
  72. "curl": curl_test,
  73. "wget": wget_test,
  74. "http": http_test,
  75. "netcat": netcat_test,
  76. "tcp": tcp_test,
  77. "redis": redis_test,
  78. "postgres": postgres_test,
  79. "mariadb": mariadb_test,
  80. "mongodb": mongodb_test,
  81. }
  82. if variant not in tests:
  83. raise RenderError(f"Test variant [{variant}] is not valid. Valid options are: [{', '.join(tests.keys())}]")
  84. return tests[variant](config)
  85. def get_key(config: dict, key: str, default: Any, required: bool):
  86. if key not in config:
  87. if not required:
  88. return default
  89. raise RenderError(f"Expected [{key}] to be set")
  90. return config[key]
  91. def curl_test(config: dict) -> list[str]:
  92. config = config or {}
  93. port = get_key(config, "port", None, True)
  94. path = valid_http_path_or_raise(get_key(config, "path", "/", False))
  95. scheme = get_key(config, "scheme", "http", False)
  96. host = get_key(config, "host", "127.0.0.1", False)
  97. headers = get_key(config, "headers", [], False)
  98. method = get_key(config, "method", "GET", False)
  99. data = get_key(config, "data", None, False)
  100. cmd = ["CMD", "curl", "--request", method, "--silent", "--output", "/dev/null", "--show-error", "--fail"]
  101. if scheme == "https":
  102. cmd.append("--insecure")
  103. for header in headers:
  104. if not header[0] or not header[1]:
  105. raise RenderError("Expected [header] to be a list of two items for curl test")
  106. cmd.extend(["--header", f"{header[0]}: {header[1]}"])
  107. if data is not None:
  108. cmd.extend(["--data", json.dumps(data)])
  109. cmd.append(f"{scheme}://{host}:{port}{path}")
  110. return cmd
  111. def wget_test(config: dict) -> list[str]:
  112. config = config or {}
  113. port = get_key(config, "port", None, True)
  114. path = valid_http_path_or_raise(get_key(config, "path", "/", False))
  115. scheme = get_key(config, "scheme", "http", False)
  116. host = get_key(config, "host", "127.0.0.1", False)
  117. headers = get_key(config, "headers", [], False)
  118. spider = get_key(config, "spider", True, False)
  119. cmd = ["CMD", "wget", "--quiet"]
  120. if spider:
  121. cmd.append("--spider")
  122. else:
  123. cmd.extend(["-O", "/dev/null"])
  124. if scheme == "https":
  125. cmd.append("--no-check-certificate")
  126. for header in headers:
  127. if not header[0] or not header[1]:
  128. raise RenderError("Expected [header] to be a list of two items for wget test")
  129. cmd.extend(["--header", f"{header[0]}: {header[1]}"])
  130. cmd.append(f"{scheme}://{host}:{port}{path}")
  131. return cmd
  132. def http_test(config: dict) -> list[str]:
  133. config = config or {}
  134. port = get_key(config, "port", None, True)
  135. path = valid_http_path_or_raise(get_key(config, "path", "/", False))
  136. host = get_key(config, "host", "127.0.0.1", False)
  137. return [
  138. "CMD-SHELL",
  139. f"""/bin/bash -c 'exec {{hc_fd}}<>/dev/tcp/{host}/{port} && echo -e "GET {path} HTTP/1.1\\r\\nHost: {host}\\r\\nConnection: close\\r\\n\\r\\n" >&${{hc_fd}} && cat <&${{hc_fd}} | grep "HTTP" | grep -q "200"'""", # noqa
  140. ]
  141. def netcat_test(config: dict) -> list[str]:
  142. config = config or {}
  143. port = get_key(config, "port", None, True)
  144. host = get_key(config, "host", "127.0.0.1", False)
  145. udp_mode = get_key(config, "udp", False, False)
  146. cmd = ["CMD", "nc", "-z", "-w", "1"]
  147. if udp_mode:
  148. cmd.append("-u")
  149. cmd.extend([host, str(port)])
  150. return cmd
  151. def tcp_test(config: dict) -> list[str]:
  152. config = config or {}
  153. port = get_key(config, "port", None, True)
  154. host = get_key(config, "host", "127.0.0.1", False)
  155. return ["CMD", "timeout", "1", "bash", "-c", f"cat < /dev/null > /dev/tcp/{host}/{port}"]
  156. def redis_test(config: dict) -> list[str]:
  157. config = config or {}
  158. port = get_key(config, "port", 6379, False)
  159. host = get_key(config, "host", "127.0.0.1", False)
  160. password = get_key(config, "password", None, False)
  161. cmd = ["CMD", "redis-cli", "-h", host, "-p", str(port)]
  162. if password:
  163. cmd.extend(["-a", password])
  164. cmd.append("ping")
  165. return cmd
  166. def postgres_test(config: dict) -> list[str]:
  167. config = config or {}
  168. port = get_key(config, "port", 5432, False)
  169. host = get_key(config, "host", "127.0.0.1", False)
  170. user = get_key(config, "user", None, True)
  171. db = get_key(config, "db", None, True)
  172. return ["CMD", "pg_isready", "-h", host, "-p", str(port), "-U", user, "-d", db]
  173. def mariadb_test(config: dict) -> list[str]:
  174. config = config or {}
  175. port = get_key(config, "port", 3306, False)
  176. host = get_key(config, "host", "127.0.0.1", False)
  177. password = get_key(config, "password", None, True)
  178. return [
  179. "CMD",
  180. "mariadb-admin",
  181. "--user=root",
  182. f"--host={host}",
  183. f"--port={port}",
  184. f"--password={password}",
  185. "ping",
  186. ]
  187. def mongodb_test(config: dict) -> list[str]:
  188. config = config or {}
  189. port = get_key(config, "port", 27017, False)
  190. host = get_key(config, "host", "127.0.0.1", False)
  191. db = get_key(config, "db", None, True)
  192. return [
  193. "CMD",
  194. "mongosh",
  195. "--host",
  196. host,
  197. "--port",
  198. str(port),
  199. db,
  200. "--eval",
  201. 'db.adminCommand("ping")',
  202. "--quiet",
  203. ]