├── Dockerfile ├── LICENSE ├── README.md ├── app.py └── requirements.txt /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.10-alpine 2 | LABEL maintainer="Kévin Darcel " 3 | 4 | WORKDIR /usr/src/docker-image-updater 5 | 6 | COPY app.py requirements.txt /usr/src/docker-image-updater/ 7 | 8 | RUN pip install --no-cache-dir -r requirements.txt 9 | 10 | ENTRYPOINT ["python", "-u", "app.py"] 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Kévin Darcel 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Docker Image Puller 2 | =================== 3 | 4 | [![](https://images.microbadger.com/badges/version/tuxity/docker-image-puller.svg)](https://hub.docker.com/r/tuxity/docker-image-puller/) 5 | ![](https://images.microbadger.com/badges/image/tuxity/docker-image-puller.svg) 6 | 7 | ## Overview 8 | 9 | If you work with docker and continuous integrations tools, you might need to update your images on your servers as soon as your build is finished. 10 | 11 | This tool is a tiny webserver listening for a `POST` and automatically update the specified image using [Docker](https://docs.docker.com/engine/reference/api/docker_remote_api/) API. 12 | 13 | You just have to run the image on your server, and configure your CI tool. 14 | 15 | CI tools to make the POST request: 16 | - [Drone](http://readme.drone.io/plugins/webhook/) 17 | 18 | 19 | ## Installation 20 | 21 | Launch the image on your server, where the images you want to update are 22 | ``` 23 | docker run -d \ 24 | --name dip \ 25 | --env TOKEN=abcd4242 \ 26 | --env REGISTRY_USER=roberto \ 27 | --env REGISTRY_PASSWD=robertopwd \ 28 | -p 8080:8080 \ 29 | -v /var/run/docker.sock:/var/run/docker.sock \ 30 | tuxity/docker-image-puller 31 | ``` 32 | 33 | Available env variable: 34 | ``` 35 | TOKEN* 36 | REGISTRY_USER 37 | REGISTRY_PASSWD 38 | REGISTRY_URL (default: https://index.docker.io/v1/) 39 | HOST (default: 0.0.0.0) 40 | PORT (default: 8080) 41 | DEBUG (default: False) 42 | ``` 43 | 44 | \* mandatory variables. For `TOKEN` You can generate a random string, it's a security measure. 45 | 46 | After, you just have to make a request to the server: 47 | ``` 48 | POST http://ipofyourserver/images/pull?token=abcd4242&restart_containers=true&image=nginx:latest 49 | ``` 50 | 51 | ## Logs 52 | 53 | You can access container logs with 54 | ``` 55 | docker logs --follow dip 56 | ```` 57 | -------------------------------------------------------------------------------- /app.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | import os, sys 5 | import click 6 | import re 7 | 8 | from flask import Flask 9 | from flask import request 10 | from flask import jsonify 11 | 12 | import docker 13 | 14 | app = Flask(__name__) 15 | client = docker.from_env() 16 | 17 | @app.route('/') 18 | def main(): 19 | return jsonify(success=True), 200 20 | 21 | @app.route('/images/pull', methods=['POST']) 22 | def image_puller(): 23 | if not request.form['token'] or not request.form['image']: 24 | return jsonify(success=False, error="Missing parameters"), 400 25 | 26 | image = request.form['image'] 27 | 28 | if request.form['token'] != os.environ['TOKEN']: 29 | return jsonify(success=False, error="Invalid token"), 403 30 | 31 | restart_containers = True if request.form['restart_containers'] == "true" else False 32 | 33 | old_containers = [] 34 | for container in client.containers.list(): 35 | if re.match( r'.*' + re.escape(image) + r'$', container.attrs['Config']['Image']): 36 | old_containers.append(container) 37 | 38 | if len(old_containers) == 0: 39 | return jsonify(success=False, error="No running containers found with the specified image"), 404 40 | 41 | print ('Updating', str(len(old_containers)), 'containers with', image, 'image') 42 | image = image.split(':') 43 | image_name = image[0] 44 | image_tag = image[1] if len(image) == 2 else 'latest' 45 | 46 | print ('\tPulling new image...') 47 | client.images.pull(image_name, tag=image_tag) 48 | 49 | if restart_containers == False: 50 | return jsonify(success=True, message=str(len(old_containers)) + " containers updated"), 200 51 | 52 | print ('\tCreating new containers...') 53 | new_containers = [] 54 | for container in old_containers: 55 | if 'HOSTNAME' in os.environ and os.environ['HOSTNAME'] == container.attrs['Id']: 56 | return jsonify(success=False, error="You can't restart the container where the puller script is running"), 403 57 | 58 | new_cont = docker.APIClient().create_container(container.attrs['Config']['Image'], environment=container.attrs['Config']['Env'], host_config=container.attrs['HostConfig']) 59 | 60 | new_containers.append(client.containers.get(new_cont['Id'])) 61 | 62 | print ('\tStopping old containers...') 63 | for container in old_containers: 64 | container.stop() 65 | 66 | print ('\tStarting new containers...') 67 | for container in new_containers: 68 | container.start() 69 | 70 | print ('\tRemoving old containers...') 71 | for container in old_containers: 72 | container.remove() 73 | 74 | return jsonify(success=True, message=str(len(old_containers)) + " containers updated and restarted"), 200 75 | 76 | @click.command() 77 | @click.option('-h', default='0.0.0.0', help='Set the host') 78 | @click.option('-p', default=8080, help='Set the listening port') 79 | @click.option('--debug', default=False, help='Enable debug option') 80 | def main(h, p, debug): 81 | if not os.environ.get('TOKEN'): 82 | print ('ERROR: Missing TOKEN env variable') 83 | sys.exit(1) 84 | 85 | registry_user = os.environ.get('REGISTRY_USER') 86 | registry_passwd = os.environ.get('REGISTRY_PASSWD') 87 | registry_url = os.environ.get('REGISTRY_URL', 'https://index.docker.io/v1/') 88 | 89 | if registry_user and registry_passwd: 90 | try: 91 | client.login(username=registry_user, password=registry_passwd, registry=registry_url) 92 | except Exception as e: 93 | print(e) 94 | sys.exit(1) 95 | 96 | app.run( 97 | host = os.environ.get('HOST', default=h), 98 | port = os.environ.get('PORT', default=p), 99 | debug = os.environ.get('DEBUG', default=debug) 100 | ) 101 | 102 | if __name__ == "__main__": 103 | main() 104 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Flask>=2.0.3 2 | click>=8.0 3 | docker>=5.0.0 4 | --------------------------------------------------------------------------------