1
0

update_manager.py 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135
  1. import subprocess
  2. import logging
  3. import os
  4. from pathlib import Path
  5. from datetime import datetime
  6. from typing import Optional, Tuple
  7. # Configure logging
  8. logger = logging.getLogger(__name__)
  9. # Trigger file location - visible to both container (/app) and host
  10. TRIGGER_FILE = Path("/app/.update-trigger")
  11. def check_git_updates():
  12. """Check for available Git updates."""
  13. try:
  14. logger.debug("Checking for Git updates")
  15. subprocess.run(["git", "fetch", "--tags", "--force"], check=True)
  16. latest_remote_tag = subprocess.check_output(
  17. ["git", "describe", "--tags", "--abbrev=0", "origin/main"]
  18. ).strip().decode()
  19. latest_local_tag = subprocess.check_output(
  20. ["git", "describe", "--tags", "--abbrev=0"]
  21. ).strip().decode()
  22. tag_behind_count = 0
  23. if latest_local_tag != latest_remote_tag:
  24. tags = subprocess.check_output(
  25. ["git", "tag", "--merged", "origin/main"], text=True
  26. ).splitlines()
  27. found_local = False
  28. for tag in tags:
  29. if tag == latest_local_tag:
  30. found_local = True
  31. elif found_local:
  32. tag_behind_count += 1
  33. if tag == latest_remote_tag:
  34. break
  35. updates_available = latest_remote_tag != latest_local_tag
  36. logger.info(f"Updates available: {updates_available}, {tag_behind_count} versions behind")
  37. return {
  38. "updates_available": updates_available,
  39. "tag_behind_count": tag_behind_count,
  40. "latest_remote_tag": latest_remote_tag,
  41. "latest_local_tag": latest_local_tag,
  42. }
  43. except subprocess.CalledProcessError as e:
  44. logger.error(f"Error checking Git updates: {e}")
  45. return {
  46. "updates_available": False,
  47. "tag_behind_count": 0,
  48. "latest_remote_tag": None,
  49. "latest_local_tag": None,
  50. }
  51. def is_update_watcher_available() -> bool:
  52. """Check if the update watcher service is running on the host.
  53. The watcher service monitors the trigger file and runs 'dw update'
  54. when it detects a trigger.
  55. """
  56. # The watcher is available if we can write to the trigger file location
  57. # and the parent directory exists (indicating proper volume mount)
  58. try:
  59. return TRIGGER_FILE.parent.exists() and os.access(TRIGGER_FILE.parent, os.W_OK)
  60. except Exception:
  61. return False
  62. def trigger_host_update(message: str = None) -> Tuple[bool, Optional[str]]:
  63. """Signal the host to run 'dw update' by creating a trigger file.
  64. The update watcher service on the host monitors this file and
  65. executes the full update process when triggered.
  66. Args:
  67. message: Optional message to include in the trigger file
  68. Returns:
  69. Tuple of (success, error_message)
  70. """
  71. try:
  72. # Write trigger file with timestamp and optional message
  73. trigger_content = f"triggered_at={datetime.now().isoformat()}\n"
  74. if message:
  75. trigger_content += f"message={message}\n"
  76. TRIGGER_FILE.write_text(trigger_content)
  77. logger.info(f"Update trigger created at {TRIGGER_FILE}")
  78. return True, None
  79. except Exception as e:
  80. error_msg = f"Failed to create update trigger: {e}"
  81. logger.error(error_msg)
  82. return False, error_msg
  83. def update_software():
  84. """Trigger a software update on the host machine.
  85. When running in Docker, this creates a trigger file that the host's
  86. update-watcher service monitors. The watcher then runs 'dw update'
  87. on the host, which properly handles:
  88. - Git pull for latest code
  89. - Docker image pulls
  90. - Container recreation with new images
  91. - Cleanup of old images
  92. Returns:
  93. Tuple of (success, error_message, error_log)
  94. """
  95. logger.info("Initiating software update...")
  96. # Check if we can trigger host update
  97. if not is_update_watcher_available():
  98. error_msg = (
  99. "Update watcher not available. The update-watcher service may not be "
  100. "installed or the volume mount is not configured correctly. "
  101. "Please run 'dw update' manually from the host machine."
  102. )
  103. logger.error(error_msg)
  104. return False, error_msg, [error_msg]
  105. # Trigger the host update
  106. success, error = trigger_host_update("Triggered from web UI")
  107. if success:
  108. logger.info("Update triggered successfully - host will process shortly")
  109. return True, None, None
  110. else:
  111. return False, error, [error]