2023-11-16 12:31:12 +01:00
|
|
|
import os
|
|
|
|
import subprocess
|
|
|
|
import sys
|
2023-11-16 15:13:34 +01:00
|
|
|
import time
|
2023-11-16 12:31:12 +01:00
|
|
|
|
|
|
|
def run_command(command):
|
2023-11-16 15:42:09 +01:00
|
|
|
process = None
|
|
|
|
try:
|
|
|
|
process = subprocess.Popen(command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
|
|
|
|
output = []
|
2023-11-16 14:24:13 +01:00
|
|
|
|
2023-11-16 15:42:09 +01:00
|
|
|
for line in iter(process.stdout.readline, b''):
|
|
|
|
decoded_line = line.decode()
|
|
|
|
output.append(decoded_line)
|
|
|
|
sys.stdout.write(decoded_line)
|
2023-11-16 14:24:13 +01:00
|
|
|
|
2023-11-16 15:42:09 +01:00
|
|
|
return_code = process.wait()
|
|
|
|
if return_code:
|
|
|
|
full_output = ''.join(output)
|
|
|
|
raise subprocess.CalledProcessError(return_code, command, output=full_output.encode())
|
|
|
|
finally:
|
|
|
|
if process and process.stdout:
|
|
|
|
process.stdout.close()
|
2023-11-16 12:31:12 +01:00
|
|
|
|
|
|
|
def git_pull(directory):
|
2023-12-11 19:24:04 +01:00
|
|
|
"""
|
|
|
|
Checks whether the Git repository in the specified directory is up to date and performs a git pull if necessary.
|
|
|
|
|
|
|
|
Args:
|
|
|
|
directory (str): The path to the directory of the Git repository.
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
bool: True if a git pull was performed, otherwise False.
|
|
|
|
"""
|
2023-11-16 12:31:12 +01:00
|
|
|
os.chdir(directory)
|
|
|
|
print(f"Checking if the git repository in {directory} is up to date.")
|
|
|
|
local = subprocess.check_output("git rev-parse @", shell=True).decode().strip()
|
|
|
|
remote = subprocess.check_output("git rev-parse @{u}", shell=True).decode().strip()
|
|
|
|
|
|
|
|
if local != remote:
|
|
|
|
print("Repository is not up to date. Performing git pull.")
|
|
|
|
run_command("git pull")
|
2023-12-11 19:24:04 +01:00
|
|
|
return True;
|
|
|
|
|
|
|
|
print("Repository is already up to date.")
|
|
|
|
return False;
|
2023-11-16 12:49:53 +01:00
|
|
|
|
2023-11-16 13:15:15 +01:00
|
|
|
def get_image_digests(directory):
|
|
|
|
compose_project = os.path.basename(directory)
|
2023-11-16 14:42:09 +01:00
|
|
|
try:
|
|
|
|
images_output = subprocess.check_output(
|
|
|
|
f'docker images --format "{{{{.Repository}}}}:{{{{.Tag}}}}@{{{{.Digest}}}}" | grep {compose_project}',
|
|
|
|
shell=True
|
|
|
|
).decode().strip()
|
|
|
|
return dict(line.split('@') for line in images_output.splitlines() if line)
|
|
|
|
except subprocess.CalledProcessError as e:
|
|
|
|
if e.returncode == 1: # grep no match found
|
|
|
|
return {}
|
|
|
|
else:
|
|
|
|
raise # Other errors are still raised
|
2023-11-16 12:31:12 +01:00
|
|
|
|
2023-11-16 15:13:34 +01:00
|
|
|
def is_any_service_up(directory):
|
|
|
|
os.chdir(directory)
|
|
|
|
process = subprocess.Popen("docker-compose ps -q", shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
|
|
|
|
output, _ = process.communicate()
|
|
|
|
service_ids = output.decode().strip().splitlines()
|
|
|
|
|
|
|
|
# Check if there are any service containers up
|
|
|
|
if not service_ids:
|
|
|
|
return False # No services are up
|
|
|
|
return True # At least one service is up
|
|
|
|
|
|
|
|
|
2023-11-16 12:31:12 +01:00
|
|
|
def update_docker(directory):
|
2023-11-16 12:49:53 +01:00
|
|
|
print(f"Checking for updates to Docker images in {directory}.")
|
|
|
|
os.chdir(directory)
|
2023-11-16 13:15:15 +01:00
|
|
|
before_digests = get_image_digests(directory)
|
2023-11-16 12:49:53 +01:00
|
|
|
print("Pulling docker images.")
|
2023-11-16 14:04:42 +01:00
|
|
|
|
2023-11-16 14:33:06 +01:00
|
|
|
need_to_build=False
|
|
|
|
|
2023-11-16 14:04:42 +01:00
|
|
|
try:
|
|
|
|
run_command("docker-compose pull")
|
|
|
|
except subprocess.CalledProcessError as e:
|
|
|
|
if "pull access denied" in e.output.decode() or "must be built from source" in e.output.decode():
|
|
|
|
print("Need to build the image from source.")
|
2023-11-16 14:33:06 +01:00
|
|
|
need_to_build=True
|
2023-11-16 14:04:42 +01:00
|
|
|
else:
|
|
|
|
print("Failed to pull images with unexpected error.")
|
|
|
|
raise
|
2023-11-16 12:49:53 +01:00
|
|
|
|
2023-11-16 14:33:06 +01:00
|
|
|
|
2023-11-16 14:04:42 +01:00
|
|
|
after_digests = get_image_digests(directory)
|
2023-11-16 15:13:34 +01:00
|
|
|
if before_digests != after_digests:
|
2023-11-16 12:49:53 +01:00
|
|
|
print("Changes detected in image digests. Rebuilding containers.")
|
2023-11-16 14:33:06 +01:00
|
|
|
need_to_build=True
|
|
|
|
|
|
|
|
if need_to_build:
|
2023-11-16 15:13:34 +01:00
|
|
|
run_command("docker-compose build")
|
|
|
|
start_docker(directory)
|
2023-11-16 12:49:53 +01:00
|
|
|
else:
|
|
|
|
print("Docker images are up to date. No rebuild necessary.")
|
2023-11-16 12:31:12 +01:00
|
|
|
|
2023-11-30 17:55:23 +01:00
|
|
|
def update_nextcloud():
|
|
|
|
print("Start Nextcloud update procedure.")
|
2024-02-06 23:45:03 +01:00
|
|
|
update_procedure("docker-compose exec -T -u www-data application /var/www/html/occ upgrade")
|
|
|
|
update_procedure("docker-compose exec -T -u www-data application /var/www/html/occ maintenance:repair")
|
2023-11-30 17:55:23 +01:00
|
|
|
update_procedure("docker-compose exec -T -u www-data application /var/www/html/occ app:update --all")
|
2024-02-06 23:45:03 +01:00
|
|
|
update_procedure("docker-compose exec -T -u www-data application /var/www/html/occ maintenance:mode --off")
|
2023-11-30 17:55:23 +01:00
|
|
|
|
2023-12-11 19:24:04 +01:00
|
|
|
def update_discourse(directory):
|
|
|
|
os.chdir(directory)
|
|
|
|
print("Start Discourse update procedure.")
|
|
|
|
update_procedure("./launcher rebuild app")
|
|
|
|
|
2023-11-30 17:55:23 +01:00
|
|
|
# This procedure waits until the container is up
|
|
|
|
def update_procedure(command):
|
2023-11-16 15:13:34 +01:00
|
|
|
max_attempts = 3
|
|
|
|
for attempt in range(max_attempts):
|
|
|
|
try:
|
2023-11-30 17:55:23 +01:00
|
|
|
run_command(command)
|
2023-11-16 15:13:34 +01:00
|
|
|
break # If the command succeeds, exit the loop
|
|
|
|
except subprocess.CalledProcessError as e:
|
|
|
|
if attempt < max_attempts - 1: # Check if it's not the last attempt
|
|
|
|
print(f"Attempt {attempt + 1} failed, retrying in 60 seconds...")
|
|
|
|
time.sleep(60) # Wait for 60 seconds before retrying
|
|
|
|
else:
|
|
|
|
print("All attempts to update Nextcloud apps have failed.")
|
|
|
|
raise # Re-raise the last exception after all attempts fail
|
|
|
|
|
|
|
|
|
|
|
|
def start_docker(directory):
|
|
|
|
if is_any_service_up(directory):
|
|
|
|
print(f"Restarting containers in {directory}.")
|
|
|
|
run_command("docker-compose up -d --force-recreate")
|
|
|
|
else:
|
|
|
|
print(f"Skipped starting. No service is up in {directory}.")
|
2023-11-16 12:31:12 +01:00
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
if len(sys.argv) < 2:
|
|
|
|
print("Please provide the path to the parent directory as a parameter.")
|
|
|
|
sys.exit(1)
|
|
|
|
|
|
|
|
parent_directory = sys.argv[1]
|
|
|
|
for dir_entry in os.scandir(parent_directory):
|
|
|
|
if dir_entry.is_dir():
|
|
|
|
dir_path = dir_entry.path
|
|
|
|
print(f"Checking for updates in: {dir_path}")
|
|
|
|
|
|
|
|
if os.path.isdir(os.path.join(dir_path, ".git")):
|
2023-12-11 19:24:04 +01:00
|
|
|
git_repository_was_pulled = git_pull(dir_path)
|
2023-11-16 12:31:12 +01:00
|
|
|
|
2023-12-11 19:24:04 +01:00
|
|
|
# Discourse is an exception and uses own update command instead of docker compose
|
|
|
|
if os.path.basename(dir_path) == "discourse":
|
|
|
|
if git_repository_was_pulled:
|
|
|
|
update_discourse(dir_path)
|
|
|
|
else:
|
|
|
|
print("Discourse update skipped. No changes in git repository.")
|
|
|
|
else:
|
|
|
|
update_docker(dir_path)
|
|
|
|
# Nextcloud needs additional update procedures
|
|
|
|
if os.path.basename(dir_path) == "nextcloud":
|
|
|
|
update_nextcloud()
|