runtime_validation.py 4.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128
  1. """Helpers for validating startup runtime expectations from `.env`."""
  2. from __future__ import annotations
  3. import os
  4. from dataclasses import dataclass
  5. from pathlib import Path
  6. from dotenv import dotenv_values
  7. _CONTAINER_RUNTIME_TARGETS = {"compose", "docker"}
  8. @dataclass(frozen=True)
  9. class RuntimeEnvironment:
  10. """Describes whether the current process is running in a container runtime."""
  11. in_container: bool
  12. in_docker: bool
  13. in_kubernetes: bool
  14. @property
  15. def label(self) -> str:
  16. if self.in_kubernetes:
  17. return "Kubernetes"
  18. if self.in_docker:
  19. return "Docker"
  20. return "host"
  21. def _read_cgroup_content() -> str:
  22. """Best-effort read of cgroup metadata for container detection."""
  23. for candidate in ("/proc/1/cgroup", "/proc/self/cgroup"):
  24. try:
  25. return Path(candidate).read_text(encoding="utf-8")
  26. except OSError:
  27. continue
  28. return ""
  29. def detect_runtime_environment(
  30. environ: dict[str, str] | None = None,
  31. ) -> RuntimeEnvironment:
  32. """Detect whether the current process is running on host, Docker, or Kubernetes."""
  33. environ = environ or os.environ
  34. cgroup_content = _read_cgroup_content().lower()
  35. in_kubernetes = bool(
  36. environ.get("KUBERNETES_SERVICE_HOST")
  37. or Path("/var/run/secrets/kubernetes.io/serviceaccount").exists()
  38. or "kubepods" in cgroup_content
  39. or "kubernetes" in cgroup_content
  40. )
  41. in_docker = bool(
  42. Path("/.dockerenv").exists()
  43. or Path("/run/.containerenv").exists()
  44. or any(
  45. marker in cgroup_content
  46. for marker in ("docker", "containerd", "libpod", "podman")
  47. )
  48. )
  49. return RuntimeEnvironment(
  50. in_container=in_kubernetes or in_docker,
  51. in_docker=in_docker,
  52. in_kubernetes=in_kubernetes,
  53. )
  54. def load_runtime_target_from_env_file(env_path: str | Path = ".env") -> str | None:
  55. """Return the raw LIGHTRAG_RUNTIME_TARGET value from the `.env` file, if present."""
  56. env_values = dotenv_values(str(env_path))
  57. runtime_target = env_values.get("LIGHTRAG_RUNTIME_TARGET")
  58. if runtime_target is None:
  59. return None
  60. return runtime_target.strip()
  61. def validate_runtime_target(
  62. runtime_target: str | None,
  63. runtime_environment: RuntimeEnvironment | None = None,
  64. ) -> tuple[bool, str | None]:
  65. """Validate `.env` runtime target against the current runtime environment."""
  66. if runtime_target is None:
  67. return True, None
  68. normalized_target = runtime_target.strip().lower()
  69. runtime_environment = runtime_environment or detect_runtime_environment()
  70. if normalized_target == "host":
  71. if runtime_environment.in_container:
  72. return (
  73. False,
  74. "Configuration error in .env: LIGHTRAG_RUNTIME_TARGET=host.\n"
  75. "This value from .env requires the server process to run on the host, "
  76. f"but the current process is running inside {runtime_environment.label}.",
  77. )
  78. return True, None
  79. if normalized_target in _CONTAINER_RUNTIME_TARGETS:
  80. if runtime_environment.in_container:
  81. return True, None
  82. return (
  83. False,
  84. f"Configuration error in .env: LIGHTRAG_RUNTIME_TARGET={runtime_target}.\n"
  85. "This value from .env requires the server process to run inside Docker or "
  86. "Kubernetes, but the current process is running on the host.",
  87. )
  88. return (
  89. False,
  90. f"Configuration error in .env: LIGHTRAG_RUNTIME_TARGET={runtime_target!r}.\n"
  91. "This value from .env must be 'host' or 'compose' (alias: 'docker').",
  92. )
  93. def validate_runtime_target_from_env_file(
  94. env_path: str | Path = ".env",
  95. runtime_environment: RuntimeEnvironment | None = None,
  96. ) -> tuple[bool, str | None]:
  97. """Load LIGHTRAG_RUNTIME_TARGET from `.env` and validate it if declared."""
  98. runtime_target = load_runtime_target_from_env_file(env_path)
  99. return validate_runtime_target(runtime_target, runtime_environment)