deps_postgres.py 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160
  1. import urllib.parse
  2. from typing import TYPE_CHECKING, TypedDict, NotRequired
  3. if TYPE_CHECKING:
  4. from render import Render
  5. from storage import IxStorage
  6. try:
  7. from .error import RenderError
  8. from .deps_perms import PermsContainer
  9. from .validations import valid_port_or_raise
  10. except ImportError:
  11. from error import RenderError
  12. from deps_perms import PermsContainer
  13. from validations import valid_port_or_raise
  14. class PostgresConfig(TypedDict):
  15. user: str
  16. password: str
  17. database: str
  18. port: NotRequired[int]
  19. volume: "IxStorage"
  20. MAX_POSTGRES_VERSION = 17
  21. class PostgresContainer:
  22. def __init__(
  23. self, render_instance: "Render", name: str, image: str, config: PostgresConfig, perms_instance: PermsContainer
  24. ):
  25. self._render_instance = render_instance
  26. self._name = name
  27. self._config = config
  28. self._data_dir = "/var/lib/postgresql/data"
  29. self._upgrade_name = f"{self._name}_upgrade"
  30. self._upgrade_container = None
  31. for key in ("user", "password", "database", "volume"):
  32. if key not in config:
  33. raise RenderError(f"Expected [{key}] to be set for postgres")
  34. port = valid_port_or_raise(self.get_port())
  35. c = self._render_instance.add_container(name, image)
  36. c.set_user(999, 999)
  37. c.healthcheck.set_test("postgres", {"user": config["user"], "db": config["database"]})
  38. c.remove_devices()
  39. c.add_storage(self._data_dir, config["volume"])
  40. common_variables = {
  41. "POSTGRES_USER": config["user"],
  42. "POSTGRES_PASSWORD": config["password"],
  43. "POSTGRES_DB": config["database"],
  44. "PGPORT": port,
  45. }
  46. for k, v in common_variables.items():
  47. c.environment.add_env(k, v)
  48. perms_instance.add_or_skip_action(
  49. f"{self._name}_postgres_data", config["volume"], {"uid": 999, "gid": 999, "mode": "check"}
  50. )
  51. repo = self._get_repo(
  52. image,
  53. (
  54. "postgres",
  55. "postgis/postgis",
  56. "pgvector/pgvector",
  57. "tensorchord/pgvecto-rs",
  58. "ghcr.io/immich-app/postgres",
  59. ),
  60. )
  61. # eg we don't want to handle upgrades of pg_vector at the moment
  62. if repo == "postgres":
  63. target_major_version = self._get_target_version(image)
  64. upg = self._render_instance.add_container(self._upgrade_name, "postgres_upgrade_image")
  65. upg.set_entrypoint(["/bin/bash", "-c", "/upgrade.sh"])
  66. upg.restart.set_policy("on-failure", 1)
  67. upg.set_user(999, 999)
  68. upg.healthcheck.disable()
  69. upg.remove_devices()
  70. upg.add_storage(self._data_dir, config["volume"])
  71. for k, v in common_variables.items():
  72. upg.environment.add_env(k, v)
  73. upg.environment.add_env("TARGET_VERSION", target_major_version)
  74. upg.environment.add_env("DATA_DIR", self._data_dir)
  75. self._upgrade_container = upg
  76. c.depends.add_dependency(self._upgrade_name, "service_completed_successfully")
  77. # Store container for further configuration
  78. # For example: c.depends.add_dependency("other_container", "service_started")
  79. self._container = c
  80. @property
  81. def container(self):
  82. return self._container
  83. def add_dependency(self, container_name: str, condition: str):
  84. self._container.depends.add_dependency(container_name, condition)
  85. if self._upgrade_container:
  86. self._upgrade_container.depends.add_dependency(container_name, condition)
  87. def _get_repo(self, image, supported_repos):
  88. images = self._render_instance.values["images"]
  89. if image not in images:
  90. raise RenderError(f"Image [{image}] not found in values. Available images: [{', '.join(images.keys())}]")
  91. repo = images[image].get("repository")
  92. if not repo:
  93. raise RenderError("Could not determine repo")
  94. if repo not in supported_repos:
  95. raise RenderError(f"Unsupported repo [{repo}] for postgres. Supported repos: {', '.join(supported_repos)}")
  96. return repo
  97. def _get_target_version(self, image):
  98. images = self._render_instance.values["images"]
  99. if image not in images:
  100. raise RenderError(f"Image [{image}] not found in values. Available images: [{', '.join(images.keys())}]")
  101. tag = images[image].get("tag", "")
  102. tag = str(tag) # Account for tags like 16.6
  103. target_major_version = tag.split(".")[0]
  104. try:
  105. target_major_version = int(target_major_version)
  106. except ValueError:
  107. raise RenderError(f"Could not determine target major version from tag [{tag}]")
  108. if target_major_version > MAX_POSTGRES_VERSION:
  109. raise RenderError(f"Postgres version [{target_major_version}] is not supported")
  110. return target_major_version
  111. def get_port(self):
  112. return self._config.get("port") or 5432
  113. def get_url(self, variant: str):
  114. user = urllib.parse.quote_plus(self._config["user"])
  115. password = urllib.parse.quote_plus(self._config["password"])
  116. creds = f"{user}:{password}"
  117. addr = f"{self._name}:{self.get_port()}"
  118. db = self._config["database"]
  119. urls = {
  120. "postgres": f"postgres://{creds}@{addr}/{db}?sslmode=disable",
  121. "postgresql": f"postgresql://{creds}@{addr}/{db}?sslmode=disable",
  122. "postgresql_no_creds": f"postgresql://{addr}/{db}?sslmode=disable",
  123. "host_port": addr,
  124. }
  125. if variant not in urls:
  126. raise RenderError(f"Expected [variant] to be one of [{', '.join(urls.keys())}], got [{variant}]")
  127. return urls[variant]