import os import json import requests import config DOCKER_COMPOSE_PATH = config.portainer_compose_dir + '/{}/docker-compose.yml' BASH_COMPANION_SCRIPT = os.getcwd() + '/docker_updater.sh' REPO_UPDATER_SCRIPT = os.getcwd() + '/update_repos.sh' class PortainerClient: def __init__(self, portainer_url, access_token): self.portainer_url = portainer_url self.access_token = access_token self.api_url = f"{self.portainer_url}/api" def _get_headers(self): return { "accept": "application/json", "X-API-Key": self.access_token } def list_stacks(self): response = requests.get(f"{self.api_url}/stacks", headers=self._get_headers()) return response.json() def get_stack(self, stack_id): response = requests.get(f"{self.api_url}/stacks/{stack_id}", headers=self._get_headers()) return response.json() def get_stack_file(self, stack_id): response = requests.get(f"{self.api_url}/stacks/{stack_id}/file", headers=self._get_headers()) return response.json() def upgrade_stack(self, stack): file_response = self.get_stack_file(stack['Id']) payload = { "pullImage": True, "prune": True, "StackFileContent": file_response["StackFileContent"], "Env": stack["Env"] } response = requests.put( f"{self.api_url}/stacks/{stack['Id']}?endpointId={stack['EndpointId']}", headers=self._get_headers(), json=payload ) return response.json() def list_stack_containers(self, endpoint, stack_name): filters = '{"label":["com.docker.compose.project=' + stack_name + '"]}' response = requests.get( f"{self.api_url}/endpoints/{endpoint}/docker/containers/json?all=1&filters={filters}", headers=self._get_headers() ) return response.json() def update_image(self, endpoint, image): response = requests.post( f"{self.api_url}/endpoints/{endpoint}/docker/images/create?fromImage={image}", headers=self._get_headers() ) try: status = json.loads(response.text.strip().split('\r\n')[-1])['status'] except Exception: return False return f"Downloaded newer image for {image}" in status def update_repositories(): for repo_path in config.git_repositories: ret = os.system(f"{REPO_UPDATER_SCRIPT} {repo_path}") if ret > 0: exit(ret) def update_local_stack(stack): project_name = stack['Name'] print(f"Updating stack: {project_name}") compose_file = DOCKER_COMPOSE_PATH.format(stack['Id']) ret = os.system(f"{BASH_COMPANION_SCRIPT} {project_name} {compose_file}") if ret > 0: exit(ret) print(f"Stack {project_name} updated!\n") if __name__ == '__main__': update_repositories() client = PortainerClient(portainer_url=config.portainer_url, access_token=config.access_token) for stack in client.list_stacks(): endpoint = stack['EndpointId'] stack_name = stack['Name'] if stack['Status'] == 1: containers = client.list_stack_containers(endpoint, stack_name) an_image_was_updated = False for c in containers: this_image_was_updated = client.update_image(endpoint, c['Image']) an_image_was_updated = an_image_was_updated or this_image_was_updated if an_image_was_updated: if endpoint == 2 and stack_name in ('portainer', 'traefik'): update_local_stack(stack) else: client.upgrade_stack(stack) os.system('docker image prune -fa')