-
Notifications
You must be signed in to change notification settings - Fork 157
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* 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
Showing
10 changed files
with
315 additions
and
0 deletions.
There are no files selected for viewing
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
*.md | ||
__pycache__ | ||
*.pyc | ||
.vscode/ | ||
.gitignore |
This file contains 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
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/ |
This file contains 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
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"] |
This file contains 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
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. |
This file contains 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
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) |
This file contains 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
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 | ||
} |
This file contains 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
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) |
This file contains 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
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') |
This file contains 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
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) |
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
docker | ||
schedule |