Skip to content

Commit

Permalink
Initial stuff (#6)
Browse files Browse the repository at this point in the history
* initial commit

* update project structure

* update logo

* update project structure

* wip

* image sha comparison, class for holding new container object

* add entrypoint check

* add docstrings

* add docstrings

* error handling

* use sha check function

* logging, camelCase for class name

* add detach key

* good progress, containers update now. todo - fix volume mappings

* volumes!

* returns

* extra host_config key removal

* spacing

* catch offline error, add restart policy

* change client for now

* instantiate clients from main

* add sweet logging

* scheduler

* scheduler

* vscode gitignore

* add defaults file

* fix quotes

* spacing

* rename, add break

* docker

* how did i do that..

* sleep adjustment

* add loggers

* put back default interval
  • Loading branch information
circa10a authored Oct 2, 2018
1 parent 15ec532 commit 772456f
Show file tree
Hide file tree
Showing 10 changed files with 315 additions and 0 deletions.
5 changes: 5 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
*.md
__pycache__
*.pyc
.vscode/
.gitignore
114 changes: 114 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class

# C extensions
*.so

# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST

# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec

# Installer logs
pip-log.txt
pip-delete-this-directory.txt

# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
.hypothesis/
.pytest_cache/

# Translations
*.mo
*.pot

# Django stuff:
*.log
local_settings.py
db.sqlite3

# Flask stuff:
instance/
.webassets-cache

# Scrapy stuff:
.scrapy

# Sphinx documentation
docs/_build/

# PyBuilder
target/

# Jupyter Notebook
.ipynb_checkpoints

# IPython
profile_default/
ipython_config.py

# pyenv
.python-version

# celery beat schedule file
celerybeat-schedule

# SageMath parsed files
*.sage.py

# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/

# Spyder project settings
.spyderproject
.spyproject

# Rope project settings
.ropeproject

# mkdocs documentation
/site

# mypy
.mypy_cache/
.dmypy.json
dmypy.json

#vscode
.vscode/
4 changes: 4 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
FROM python:3-alpine
COPY . /
RUN pip install -r requirements.txt
ENTRYPOINT ["python", "ouroboros/main.py"]
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
![alt text](https://i.imgur.com/kYbI9Hi.png)

A python alternative to [watchtower](https://github.com/v2tec/watchtower)

Automatically update your running Docker containers to the latest available image.
103 changes: 103 additions & 0 deletions ouroboros/container.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import docker
import logging
import defaults
from main import api_client

class NewContainerProperties:
def __init__(self, old_container, new_image):
"""
Store object for spawning new container in place of the one with outdated image
"""
self.name = old_container['Names'][0].replace('/','')
self.image = new_image
self.command = old_container['Command']
self.ports = self.get_container_ports(old_container['Ports'])
self.host_config = api_client.create_host_config(port_bindings=self.create_host_port_bindings(old_container['Ports']),
binds=self.create_host_volume_bindings(old_container['Mounts']),
restart_policy=defaults.RESTART_POLICY)
self.labels = old_container['Labels']
self.networking_config = api_client.create_networking_config({ self.get_network_name(old_container['NetworkSettings']['Networks']): api_client.create_networking_config() })
self.volumes = self.get_volumes(old_container['Mounts'])
self.detach = True
if 'Entrypoint' in old_container:
self.entrypoint = old_container['Entrypoint']

def get_network_name(self, net_config):
"""Get first container network name (Only supports 1 network)"""
return next(iter(net_config))

def get_container_ports(self, port_list):
"""Get exposed container ports"""
container_port_list = []
for i in port_list:
container_port_list.append(i['PrivatePort'])
return container_port_list

def create_host_port_bindings(self, port_list):
"""Create host_config port bindings dictionary"""
port_bindings = {}
for port in port_list:
port_bindings.update({ port['PrivatePort']:port['PublicPort'] })
return port_bindings

def get_volumes(self, volume_list):
"""Get mapped container volumes"""
container_volume_list = []
for volume in volume_list:
container_volume_list.append('{}:{}'.format(volume['Source'], volume['Destination']))
return container_volume_list

def create_host_volume_bindings(self, volume_list):
"""Create host_config volume bindings dictionary"""
volume_bindings = {}
volume_source = ''
for volume in volume_list:
if 'Name' in volume:
volume_source = volume['Name']
else:
volume_source = volume['Source']
volume_bindings.update({
volume_source: {
'bind': volume['Destination'],
'mode': volume['Mode']
}
})
return volume_bindings

def running():
try:
return api_client.containers()
except:
logging.critical(('Can\'t connect to Docker API at {}').format(api_client.base_url))

def to_monitor():
"""Return container object list"""
container_list = []
for container in running():
container_list.append(get_name(container))
logging.debug(('Monitoring containers: {}').format(container_list))
return running()

def get_name(container_object):
"""Parse out first name of container"""
return container_object['Names'][0].replace('/','')

def stop(container_object):
"""Stop out of date container"""
logging.debug(('Stopping container: {}').format(get_name(container_object)))
return api_client.stop(container_object)

def remove(container_object):
"""Remove out of date container"""
logging.debug(('Removing container: {}').format(get_name(container_object)))
return api_client.remove_container(container_object)

def create_new_container(config):
"""Create new container with latest image"""
logging.debug(('Creating new container with opts: {}').format(config))
return api_client.create_container(**config)

def start(container_object):
"""Start newly created container with latest image"""
logging.debug(('Starting container: {}').format(container_object))
return api_client.start(container_object)
6 changes: 6 additions & 0 deletions ouroboros/defaults.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
INTERVAL = 300
LOCAL_UNIX_SOCKET = 'unix://var/run/docker.sock'
RESTART_POLICY = {
'name': 'on-failure',
'MaximumRetryCount': 1
}
18 changes: 18 additions & 0 deletions ouroboros/image.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import docker
import main
import logging

def pull_latest(image):
"""Return tag of latest image pulled"""
latest_image = image.tags[0].split(':')[0] + ':latest'
logging.debug(('Pulling image: {}').format(latest_image))
return main.client.images.pull(latest_image)

def is_up_to_date(old_sha, new_sha):
"""Returns boolean if old and new image digests match"""
return old_sha == new_sha

def remove(old_image):
"""Deletes old image after container is updated"""
logging.info(('Removing image: {}').format(old_image.tags[0]))
return main.client.images.remove(old_image.id)
15 changes: 15 additions & 0 deletions ouroboros/logger.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import logging

def set_logger(level='info'):
"""Set log level (Default=info"""
levels = {
'notset': logging.NOTSET,
'debug': logging.DEBUG,
'info': logging.INFO,
'warn': logging.WARN,
'error': logging.ERROR,
'critical': logging.CRITICAL
}
if level not in levels:
level = 'info'
return logging.basicConfig(format='[%(levelname)-s] %(asctime)s %(message)s', level=(levels.get(level.lower())), datefmt='%Y-%m-%d %H:%M:%S')
43 changes: 43 additions & 0 deletions ouroboros/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
#!/usr/bin/env python3
import docker
import schedule
import time
import datetime
import logging
import container
import image
import defaults
from logger import set_logger

client = docker.DockerClient(base_url=defaults.LOCAL_UNIX_SOCKET)
api_client = docker.APIClient(base_url=defaults.LOCAL_UNIX_SOCKET)

def main():
if not container.running():
logging.info('No containers are running')
else:
for running_container in container.to_monitor():
current_image = client.images.get(running_container['ImageID'])
try:
latest_image = image.pull_latest(current_image)
except docker.errors.APIError as e:
logging.error(e)
continue
# if current running container is running latest image
if not image.is_up_to_date(current_image.id, latest_image.id):
logging.info(('{} will be updated').format(container.get_name(running_container)))
# new container object to create new container from
new_config = container.NewContainerProperties(running_container, latest_image.tags[0])
container.stop(running_container)
container.remove(running_container)
new_container = container.create_new_container(new_config.__dict__)
container.start(new_container)
image.remove(current_image)
logging.info('All containers up to date')

if __name__ == "__main__":
set_logger('debug')
schedule.every(defaults.INTERVAL).seconds.do(main)
while True:
schedule.run_pending()
time.sleep(defaults.INTERVAL - 5)
2 changes: 2 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
docker
schedule

0 comments on commit 772456f

Please sign in to comment.