Last active
April 30, 2019 06:54
-
-
Save geberl/1a0e8c18ff3f63555eeff553ccb8cfbc to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#! /usr/bin/python | |
# -*- coding: utf-8 -*- | |
import datetime | |
import json | |
import logging | |
import requests | |
from urllib import urlencode | |
from urllib2 import Request, urlopen | |
from tzlocal import get_localzone | |
from dateutil import parser | |
# Settings. | |
PUSH_GRACE_PERIOD = datetime.timedelta(hours=18) | |
LOCAL_TIMEZONE = get_localzone() | |
DOCKER_API_USER = 'your-username' | |
DOCKER_API_PASS = 'supers3cret' | |
MAILGUN_DOMAIN_NAME = 'your-domain.com' | |
MAILGUN_API_KEY = 'please-get-your-own-free-account' | |
# Logging config. | |
logger = logging.getLogger() | |
handler = logging.StreamHandler() | |
formatter = logging.Formatter('%(asctime)s %(levelname)-8s %(message)s') | |
handler.setFormatter(formatter) | |
logger.addHandler(handler) | |
logger.setLevel(logging.INFO) | |
class BaseImage(object): | |
def __init__(self, name, watch_tags, pulled_datetime, child_images, notify): | |
""" | |
:param name: The name of the image (can be chosen freely, not in any logic) | |
:param watch_tags: The tags to watch for pushes, must be named exactly as on Docker Hub, must refer to the same | |
data/layers underneath (size bytes checked if still identical) | |
:param pulled_datetime: The date/time of the pushed version that we did pull (and use for our images); | |
datestamps in API and Portainer/`docker image ls ...` don't match exactly, within PUSH_GRACE_PERIOD is | |
treated as not-newer | |
:param child_images: The tags of the images that use that image as their base image, and that should be rebuilt | |
on update of the base image; can be chosen freely, only used as text in the notification, not in any logic | |
:param notify: Email addresses to send notifications to if an update is available | |
""" | |
self.name = name | |
self.watch_tags = watch_tags | |
self.pulled_datetime = pulled_datetime | |
self.our_images = child_images | |
self.notify = notify | |
self.docker_api_token = '' | |
def get_token(self): | |
login_url = 'https://hub.docker.com/v2/users/login/' | |
login_post_data = {'username': DOCKER_API_USER, 'password': DOCKER_API_PASS} | |
request = Request(login_url, urlencode(login_post_data).encode()) | |
data = urlopen(request).read() | |
data_dict = json.loads(data.decode()) | |
self.docker_api_token = data_dict['token'] | |
def check(self): | |
self.get_token() | |
if '/' in self.watch_tags[0]: | |
maintainer_name = self.watch_tags[0][:self.watch_tags[0].find('/')] | |
image_name = self.watch_tags[0][self.watch_tags[0].find('/')+1:self.watch_tags[0].find(':')] | |
else: | |
maintainer_name = 'library' # Docker Inc. maintained official image | |
image_name = self.watch_tags[0][:self.watch_tags[0].find(':')] | |
messages = [] | |
first_image_bytes = None | |
for n, image_name_tag in enumerate(self.watch_tags): | |
image_tag = image_name_tag[image_name_tag.find(':')+1:] | |
image_tag_url = 'https://hub.docker.com/v2/repositories/%s/%s/tags/%s' % \ | |
(maintainer_name, image_name, image_tag) | |
request = Request(image_tag_url) | |
request.add_header('Authorization', 'JWT %s' % self.docker_api_token) | |
data = urlopen(request).read() | |
data_dict = json.loads(data.decode()) | |
if n == 0: | |
first_image_bytes = data_dict['full_size'] | |
else: | |
this_image_bytes = data_dict['full_size'] | |
# Size has to be identical across tags | |
if first_image_bytes != this_image_bytes: | |
messages.append('Image sizes differ! There might be a new release.') | |
messages.append('- %s -> %s bytes' % (self.watch_tags[0], first_image_bytes)) | |
messages.append('- %s -> %s bytes' % (image_name_tag, this_image_bytes)) | |
break | |
# Pull date is ahead upload date by about 13-15h. No idea what's going on there. | |
# Our pull date + PUSH_GRACE_PERIOD has to be smaller than the image upload date. Otherwise new version. | |
image_updated = parser.parse(data_dict['last_updated']) | |
pulled_datetime_with_grace = self.pulled_datetime + PUSH_GRACE_PERIOD | |
if pulled_datetime_with_grace < image_updated: | |
messages.append('New version!') | |
messages.append('- %s -> Ours' % self.tz_datetime_string(self.pulled_datetime)) | |
messages.append('- %s -> Current' % self.tz_datetime_string(image_updated)) | |
break | |
# Note: The different tags of same image are not uploaded at the same time, no point in checking them | |
if len(messages) > 0: | |
messages.insert(0, 'Watched tags: %s' % ', '.join(self.watch_tags)) | |
messages.insert(1, 'Link to repo: https://hub.docker.com/r/%s/%s/' % (maintainer_name, image_name)) | |
messages.insert(2, 'Pulled datetime: %s' % self.tz_datetime_string(self.pulled_datetime)) | |
messages.insert(3, 'Child images: %s' % ', '.join(self.our_images)) | |
messages.insert(4, '\n') | |
messages.append('\n') | |
messages.append('This email was sent to: %s' % ', '.join(self.notify)) | |
self.send_email_mailgun('[email protected]', | |
self.notify, | |
'Docker Image Update Alert: "%s"' % self.name, | |
'\n'.join(messages)) | |
logger.info('Update found for "%s", emails sent.' % self.name) | |
else: | |
logger.info('No update found for "%s".' % self.name) | |
@staticmethod | |
def tz_datetime_string(datetime_obj): | |
return datetime_obj.strftime('%Y-%m-%d %H:%M:%S %z (%Z)') | |
@staticmethod | |
def send_email_mailgun(from_addr, to_addr_list, subject, message_text): | |
""" | |
Send emails via Mailgun's API | |
Same code works locally in PyCharm as well as on Digitalocean or Hetzner. | |
Limit 10000 mails per month in free tier of service. | |
https://documentation.mailgun.com/api-sending.html#examples | |
""" | |
response = requests.post('https://api.mailgun.net/v3/%s/messages' % MAILGUN_DOMAIN_NAME, | |
auth=('api', MAILGUN_API_KEY), | |
data={'from': from_addr, | |
'to': to_addr_list, | |
'subject': subject, | |
'text': message_text}) | |
if response.status_code != 200: | |
return 'Error sending mail! (code %s) (%s)' % (response.status_code, response.text) | |
if __name__ == '__main__': | |
our_base_images = [] | |
our_base_images.append(BaseImage(name='debian:stretch', | |
watch_tags=['debian:stretch', | |
'debian:latest'], | |
pulled_datetime=datetime.datetime(2018, 7, 17, 2, 27, 24, tzinfo=LOCAL_TIMEZONE), | |
child_images=[], | |
notify=['[email protected]'])) | |
our_base_images.append(BaseImage(name='debian:stretch-slim', | |
watch_tags=['debian:stretch-slim'], | |
pulled_datetime=datetime.datetime(2018, 7, 17, 2, 28, 4, tzinfo=LOCAL_TIMEZONE), | |
child_images=['your-namespace/your-image:latest'], | |
notify=['[email protected]'])) | |
# Method does not work for the openjdk or jenkins images. Commented out for now. | |
# Example: API shows push date of "2018-08-03 23:53:29 +0000" | |
# Our image is from "2018-07-17 08:20:21 +0200" | |
# Manifest v1 json shows "2018-07-17 06:20:21 +0000" (in history, topmost item) | |
# A docker pul says "Image is up to date" | |
# Maybe push datetime in API is for different architecture of image? No way to get that info for now though. | |
for base_image in our_base_images: | |
base_image.check() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment