Compare commits

...

30 Commits

Author SHA1 Message Date
e885d5dc27 Correcting url used for portainer ping 2024-04-30 15:20:56 +02:00
10a4c05912 Correcting type in put method to get uri 2024-04-25 11:08:49 +02:00
02f73bcf27 Merge branch 'master' of git.dorfsvald.net:ewandor/portainer_updater 2024-04-25 11:00:52 +02:00
7dedfdb5d7 Adding function to wait for server to be back online after core element upgrade 2024-04-25 10:54:06 +02:00
f234ef2015 Adding generic requests method 2024-04-25 10:49:47 +02:00
8a84a70c59 Adding dynamic interpreter selector and real script path finding 2024-02-15 10:55:53 +01:00
aa60dd2529 Adding endpoint separation to allow image purging per endpoint 2023-12-02 01:50:01 +01:00
3b6f13abc4 Changing local updater sequence 2023-12-01 20:45:06 +01:00
9b5a45290a Improving logging further 2023-12-01 20:40:10 +01:00
a64fc40a16 Merge remote-tracking branch 'origin/master' 2023-12-01 20:34:47 +01:00
5b3b02980c Adding logs in the process 2023-12-01 17:18:53 +01:00
bbbf4ce497 Using Portainer API to update images 2023-12-01 17:12:03 +01:00
3b79e59076 Adding quotes in bash 2023-11-24 21:04:05 +01:00
91b8d35220 Modifying success logs 2023-10-03 13:40:34 +02:00
6dd7f5bff7 Adding logs printing 2023-10-03 13:37:24 +02:00
335f0c6f50 Correcting script updater name 2023-10-03 13:26:22 +02:00
38f3fba33c Merge branch 'master' of git.dorfsvald.net:ewandor/portainer_updater 2023-10-03 13:25:15 +02:00
400d4231d0 Exit if return value diff than 0 2023-10-03 13:24:50 +02:00
59ea60576f Calling the repository updater method 2023-10-03 13:20:10 +02:00
1a481776c7 Ignoring venv folder 2023-10-03 13:16:18 +02:00
cc7d62d975 Adding the possibility to pull changes from git before updating images 2023-10-03 13:13:51 +02:00
d58f26d49c Using url from config file 2023-10-03 13:13:22 +02:00
ef7e5956ed Removing config file from repo 2023-10-03 13:12:01 +02:00
acd0572d6b Using embeded version of docker compose for dash compatibility 2023-10-03 13:11:08 +02:00
cd9a3ea255 Pruning all unused docker images, inclunding non dangling 2023-09-21 17:39:45 +02:00
56f303cce7 Pruning all unused docker images 2023-09-21 17:36:58 +02:00
c3e4b04f20 Correcting current dir 2023-09-21 17:15:26 +02:00
c8e7674256 Updating documentation 2023-09-21 17:09:05 +02:00
dda3414428 Adding a configuration for portainer data path 2023-09-21 17:08:45 +02:00
1b3f577486 Adding a requirements file 2023-09-21 17:07:37 +02:00
8 changed files with 186 additions and 21 deletions

3
.gitignore vendored
View File

@@ -1,3 +1,4 @@
__pycache__/
venv
.idea/
config.py

12
README.md Normal file
View File

@@ -0,0 +1,12 @@
# Portainer Updater
Calls portainer's API and updates the images of all currently active stacks
## Usage
Update configuration file with your portainer access token and the path of portainer compose directory
After restarting all container, it prunes all unused images, including non dangling ones
You can then simply execute:
python main.py

7
config.example.py Normal file
View File

@@ -0,0 +1,7 @@
portainer_url = 'https://portainer.dorfsvald.net'
portainer_access_token = 'example_access_token'
portainer_compose_dir = '/absolute/path/to/portainer-data/compose'
git_repositories = [
'path/to/a/git/repository',
'path/to/another/git/repo'
]

View File

@@ -1 +0,0 @@
access_token = 'ptr_2Wx6aA26hEEcsPbi2iVyRcuDt6/9tjHlkg+7z9btkjw='

View File

@@ -5,6 +5,6 @@ set -e
PROJECT_NAME=$1
COMPOSE_FILE=$2
docker-compose -p $PROJECT_NAME -f $COMPOSE_FILE stop
docker-compose -p $PROJECT_NAME -f $COMPOSE_FILE pull
docker-compose -p $PROJECT_NAME -f $COMPOSE_FILE up -d
docker compose -p "$PROJECT_NAME" -f "$COMPOSE_FILE" pull
docker compose -p "$PROJECT_NAME" -f "$COMPOSE_FILE" down
docker compose -p "$PROJECT_NAME" -f "$COMPOSE_FILE" up -d

171
main.py Normal file → Executable file
View File

@@ -1,31 +1,170 @@
#!/usr/bin/env python3
import os
import json
from time import sleep
import requests
import config
DOCKER_COMPOSE_PATH = '/srv/docker/portainer/portainer-data/compose/{}/docker-compose.yml'
CURRENT_DIR = os.path.dirname(__file__)
BASH_COMPANION_SCRIPT = CURRENT_DIR + '/docker_updater.sh'
DOCKER_COMPOSE_PATH = config.portainer_compose_dir + '/{}/docker-compose.yml'
SCRIPT_DIR = os.path.dirname(__file__)
BASH_COMPANION_SCRIPT = f'{SCRIPT_DIR}/docker_updater.sh'
REPO_UPDATER_SCRIPT = f'{SCRIPT_DIR}/update_repos.sh'
def update_stack(stack):
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 _get(self, uri):
response = requests.get(f"{self.api_url}/{uri}", headers=self._get_headers())
return response.json()
def list_endpoints(self):
return self._get("endpoints")
def list_stacks(self, endpoint_id=None):
filters = f'?filters={{"EndpointID":{endpoint_id}}}' if endpoint_id else ""
return self._get(f'stacks{filters}')
def get_stack(self, stack_id):
return self._get(f'stacks/{stack_id}')
def get_stack_file(self, stack_id):
return self._get(f'stacks/{stack_id}/file')
def _put(self, uri, payload):
response = requests.put(
f"{self.api_url}/{uri}",
headers=self._get_headers(),
json=payload
)
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"]
}
return self._put(f"stacks/{stack['Id']}?endpointId={stack['EndpointId']}", payload)
def list_stack_containers(self, endpoint, stack_name):
filters = '{"label":["com.docker.compose.project=' + stack_name + '"]}'
return self._get(f'endpoints/{endpoint}/docker/containers/json?all=1&filters={filters}')
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 delete_image(self, endpoint, image):
response = requests.delete(
f"{self.api_url}/endpoints/{endpoint}/docker/images/{image}?force=false",
headers=self._get_headers()
)
def ping_server(self):
try:
r = requests.get(f"{self.portainer_url}/", headers=self._get_headers())
except requests.exceptions.ConnectionError:
return False
return r.status_code == 200
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_stack_images(endpoint, stack_name):
containers = client.list_stack_containers(endpoint, stack_name)
updated_images = []
for c in containers:
if client.update_image(endpoint, c['Image']):
print(f"{c['Image']} was updated.")
updated_images.append(c['ImageID'])
return updated_images
def upgrade_local_stack(stack):
project_name = stack['Name']
print(f"Updating stack: {project_name}")
compose_file = DOCKER_COMPOSE_PATH.format(stack['Id'])
os.system(f"{BASH_COMPANION_SCRIPT} {project_name} {compose_file}")
ret = os.system(f"{BASH_COMPANION_SCRIPT} {project_name} {compose_file}")
if ret > 0:
exit(ret)
print(f"Stack {project_name} updated!\n")
def upgrade_endpoint_stack(client, endpoint):
endpoint_id = endpoint['Id']
endpoint_updated_images = []
for stack in client.list_stacks(endpoint_id):
if stack['Status'] == 1:
stack_name = stack['Name']
print(f"Checking {stack_name} for updates.")
updated_images = update_stack_images(endpoint_id, stack_name)
if updated_images:
endpoint_updated_images += updated_images
print(f"{stack_name} was updated. Restarting...")
if endpoint_id == 2 and stack_name in ('portainer', 'traefik'):
upgrade_local_stack(stack)
while not client.ping_server():
print("Waiting for server to be back online...")
sleep(1)
else:
client.upgrade_stack(stack)
print(f"{stack_name} restarted.")
else:
print(f"{stack_name} is up to date. Skipping.")
if endpoint_updated_images:
print("Deleting obsolete images")
for image_id in endpoint_updated_images:
client.delete_image(endpoint_id, image_id)
else:
print("No obsolete image to delete")
if __name__ == '__main__':
headers = {
"accept": "application/json",
"X-API-Key": config.access_token
}
update_repositories()
api_url = "https://portainer.dorfsvald.net/api/stacks"
response = requests.get(api_url, headers=headers)
stack_list = response.json()
client = PortainerClient(portainer_url=config.portainer_url, access_token=config.access_token)
for s in stack_list:
if s['Status'] == 1:
update_stack(s)
for e in client.list_endpoints():
upgrade_endpoint_stack(client, e)

1
requirements.txt Normal file
View File

@@ -0,0 +1 @@
requests

6
update_repos.sh Executable file
View File

@@ -0,0 +1,6 @@
REPO_PATH=$1
git -C "$REPO_PATH" fetch --tags
LAST_HASH=$(git -C "$REPO_PATH" rev-list --tags --max-count=1)
LAST_TAG=$(git -C "$REPO_PATH" describe --tags "$LAST_HASH")
git -C "$REPO_PATH" checkout "$LAST_TAG"