diff --git a/README.md b/README.md index 390da9c..eadf0f6 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ [![Gitlab Project](https://img.shields.io/badge/GitLab-Project-554488.svg)](https://gitlab.com/ix.ai/cioban/) -A docker swarm service for automatically updating your services to the latest image tag push. You can enable telegram notifications, so you get a message after every successful update. +A docker swarm service for automatically updating your services to the latest image tag push. You can enable telegram or gotify notifications, so you get a message after every successful update. ## Usage Examples @@ -52,6 +52,8 @@ Cioban will try to update your services every 5 minutes by default. The followin | `FILTER_SERVICES` | - | Anything accepted by the filtering flag in `docker service ls`. Example: `label=ai.ix.auto-update=true` | | `TELEGRAM_TOKEN` | - | See the [Telegram documentation](https://core.telegram.org/bots#creating-a-new-bot) how to get a new token | | `TELEGRAM_CHAT_ID` | - | See this question on [stackoverflow](https://stackoverflow.com/questions/32423837/telegram-bot-how-to-get-a-group-chat-id) | +| `GOTIFY_URL` | - | The URL of the [Gotify](https://gotify.net/) server | +| `GOTIFY_TOKEN` | - | The APP token for Gotify | | `NOTIFY_INCLUDE_NEW_IMAGE` | - | Set this variable to anything to include the new image (including digest) in the update notification | | `NOTIFY_INCLUDE_OLD_IMAGE` | - | Set this variable to anything to include the old image (including digest) in the update notification | | `LOGLEVEL` | `INFO` | [Logging Level](https://docs.python.org/3/library/logging.html#levels) | diff --git a/cioban/__main__.py b/cioban/__main__.py index 878b82e..c9157b9 100644 --- a/cioban/__main__.py +++ b/cioban/__main__.py @@ -23,58 +23,57 @@ filters = os.environ.get('FILTER_SERVICES') filters = filters.split('=', 1) options['filters'] = {filters[0]: filters[1]} - log.info('FILTER_SERVICES: "{}"'.format(options['filters'])) - else: - log.info('FILTER_SERVICES not set') + log.info(f"FILTER_SERVICES: '{options['filters']}'") if os.environ.get('BLACKLIST_SERVICES'): blacklist = os.environ.get('BLACKLIST_SERVICES') options['blacklist'] = blacklist.split(' ') - log.info('BLACKLIST_SERVICES: "{}"'.format(options['blacklist'])) - else: - log.info('BLACKLIST_SERVICES not set') + log.info(f"BLACKLIST_SERVICES: '{options['blacklist']}'") if os.environ.get('TELEGRAM_TOKEN'): options['telegram_token'] = os.environ.get('TELEGRAM_TOKEN') log.info('TELEGRAM_TOKEN is set') - else: - log.info('TELEGRAM_TOKEN not set') if os.environ.get('TELEGRAM_CHAT_ID'): options['telegram_chat_id'] = os.environ.get('TELEGRAM_CHAT_ID') log.info('TELEGRAM_CHAT_ID is set') - else: - log.info('TELEGRAM_CHAT_ID not set') + + if os.environ.get('GOTIFY_URL'): + options['gotify_url'] = os.environ.get('GOTIFY_URL') + log.info(f"GOTIFY_URL: {options['gotify_url']}") + + if os.environ.get('GOTIFY_TOKEN'): + options['gotify_token'] = os.environ.get('GOTIFY_TOKEN') + log.info(f"GOTIFY_TOKEN is set") if os.environ.get('NOTIFY_INCLUDE_NEW_IMAGE'): options['notify_include_new_image'] = True log.info('NOTIFY_INCLUDE_NEW_IMAGE is set') - else: - log.info('NOTIFY_INCLUDE_NEW_IMAGE not set') if os.environ.get('NOTIFY_INCLUDE_OLD_IMAGE'): options['notify_include_old_image'] = True log.info('NOTIFY_INCLUDE_OLD_IMAGE is set') - else: - log.info('NOTIFY_INCLUDE_OLD_IMAGE not set') if options.get('telegram_token') and options.get('telegram_chat_id'): options['notifiers'].append('telegram') + if options.get('gotify_url') and options.get('gotify_token'): + options['notifiers'].append('gotify') + options['sleep_time'] = os.environ.get('SLEEP_TIME', '5m') - log.info('SLEEP_TIME: {}'.format(options['sleep_time'])) + log.info(f"SLEEP_TIME: {options['sleep_time']}") options['prometheus_port'] = int(os.environ.get('PORT', 9308)) - log.info('PORT: {}'.format(options['prometheus_port'])) - - log.warning( - "Starting {} {}-{} with prometheus metrics on port {}".format( - __package__, - constants.VERSION, - constants.BUILD, - options['prometheus_port'], - ) + log.info(f"PORT: {options['prometheus_port']}") + + startup_message = "Starting {} {}-{} with prometheus metrics on port {}".format( + __package__, + constants.VERSION, + constants.BUILD, + options['prometheus_port'], ) + log.warning(startup_message) c = cioban.Cioban(**options) + c.notify(title="CIOBAN Startup", message=startup_message) c.run() diff --git a/cioban/cioban.py b/cioban/cioban.py index 6acf23b..6c770a2 100644 --- a/cioban/cioban.py +++ b/cioban/cioban.py @@ -3,6 +3,7 @@ """ A docker swarm service for automatically updating your services to the latest image tag push. """ import logging +import requests import pause import docker from prometheus_client import start_http_server @@ -21,8 +22,6 @@ class Cioban(): 'sleep_time': '5m', 'prometheus_port': 9308, 'notifiers': [], - 'telegram_chat_id': None, - 'telegram_token': None, 'notify_include_new_image': False, 'notify_include_old_image': False, } @@ -34,7 +33,7 @@ def __init__(self, **kwargs): if k in self.settings: self.settings[k] = v else: - log.debug(f'{k} not found in settings') + log.debug(f'{k} not found in settings. Ignoring.') prometheus.PROM_INFO.info({'version': f'{constants.VERSION}'}) @@ -66,7 +65,6 @@ def __init__(self, **kwargs): if notifier.lower() in k.lower(): notifier_options.update({k.lower(): v}) self.notifiers.register(notifier, **notifier_options) - log.debug('Registered {}'.format(notifier)) log.debug('Cioban initialized') @@ -88,7 +86,7 @@ def __get_updated_image(self, image, image_sha): updated_image = None try: registry_data = self.docker.images.get_registry_data(image) - except docker.errors.APIError as error: + except (docker.errors.APIError, requests.exceptions.ReadTimeout) as error: log.error(f'Failed to retrieve the registry data for {image}. The error: {error}') if registry_data: @@ -191,3 +189,7 @@ def get_services(self): log.debug(f'Blacklisted {blacklist_service}') services.remove(service) return services + + def notify(self, **kwargs): + """ Sends a notification through the registered notifiers """ + self.notifiers.notify(**kwargs) diff --git a/cioban/notifiers/core.py b/cioban/notifiers/core.py index 1ac4349..808e5bd 100644 --- a/cioban/notifiers/core.py +++ b/cioban/notifiers/core.py @@ -27,6 +27,7 @@ class CoreNotifiers(): notifiers = [ 'telegram', + 'gotify', ] registered = [] @@ -37,11 +38,12 @@ def register(self, notifier, **kwargs): for n in self.notifiers: if n == notifier: instance = importlib.import_module(f'cioban.notifiers.{notifier}_notifier') - self.registered.append(instance.Notifier(**kwargs)) + self.registered.append({notifier: instance.Notifier(**kwargs)}) log.debug(f'Registered {notifier}') def notify(self, **kwargs): """ dispatches a notification to the registered notifiers """ - log.debug('Sending notification') - for notifier in self.registered: - notifier.notify(**kwargs) + for i in self.registered: + for notifier in i: + log.debug(f'Sending notification to {notifier}') + i[notifier].notify(**kwargs) diff --git a/cioban/notifiers/gotify_notifier.py b/cioban/notifiers/gotify_notifier.py new file mode 100644 index 0000000..7e1d422 --- /dev/null +++ b/cioban/notifiers/gotify_notifier.py @@ -0,0 +1,51 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" Gotify """ + +import logging +from urllib.parse import urljoin +import requests +from . import core + +log = logging.getLogger('cioban') + + +class Notifier(): + """ The notify class """ + + def __init__(self, **kwargs): + self.token = kwargs['gotify_token'] + self.url = urljoin(kwargs['gotify_url'], f'/message?token={self.token}') + log.debug(f"Initialized with {kwargs['gotify_url']}") + + def __post_message(self, title, message): + """ sends the notification to telegram """ + try: + resp = requests.post(self.url, json={ + 'title': title, + 'message': message, + }) + except requests.exceptions.RequestException as e: + # Print exception if reqeuest fails + log.error(f'Could not connect to Gotify server. The error: {e}') + + # Print request result if server returns http error code + if resp.status_code is not requests.codes.ok: # pylint: disable=no-member + log.error(f'{bytes.decode(resp.content)}') + else: + log.info("Sent message to gotify") + log.debug(f"Message: {message}") + + def notify(self, title="CIOBAN: Service Updated", **kwargs): + """ parses the arguments, formats the message and dispatches it """ + log.debug('Sending message to gotify') + message = "" + if kwargs.get('message'): + message = kwargs['message'] + else: + for k, v in kwargs.items(): + message += f'{core.key_to_title(k)}: {v}\n' + self.__post_message(title, message) + + def noop(self): + """ Does nothing """ diff --git a/cioban/notifiers/telegram_notifier.py b/cioban/notifiers/telegram_notifier.py index bfb76bf..69f47d6 100644 --- a/cioban/notifiers/telegram_notifier.py +++ b/cioban/notifiers/telegram_notifier.py @@ -58,9 +58,12 @@ def __post_message(self, message): def notify(self, title="CIOBAN: Service Updated", **kwargs): """ parses the arguments, formats the message and dispatches it """ log.debug('Sending notification to telegram') - message = '{0} {1} {0}\n'.format(u'\U00002611', title) - for k, v in kwargs.items(): - message += '{}: {}\n'.format(core.key_to_title(k), v) + message = f'{title}\n' + if kwargs.get('message'): + message += kwargs['message'] + else: + for k, v in kwargs.items(): + message += f'{core.key_to_title(k)}: {v}\n' self.__post_message(message) def noop(self):