Compare commits

..

No commits in common. "master" and "master" have entirely different histories.

13 changed files with 26 additions and 242 deletions

3
.gitignore vendored
View File

@ -1,3 +0,0 @@
__pycache__/
updates.json

View File

@ -1,2 +1,2 @@
FROM alpine:3.7 FROM alpine:3.7
RUN apk add --update --no-cache py3-yaml bash && pip3 install docker-compose requests packaging RUN apk add --update --no-cache py3-yaml bash && pip3 install docker-compose

View File

@ -4,7 +4,6 @@ services:
image: docker.clkl.de/docker/update:0.1_alpine image: docker.clkl.de/docker/update:0.1_alpine
build: . build: .
volumes: volumes:
# - /opt/docker/services:/services - ./docker-compose.py:/docker-compose.py
- ./sample:/services - ./sample:/services
- ./:/out/ command: bash -c 'python3 /docker_compose.py /services/*'
command: bash -c 'python3 /out/show_updateable.py -s -o /out/updates.json /services/* --ignore zammad'

View File

@ -41,7 +41,7 @@ def parse_dockerfile(build):
return [f] return [f]
keyword = "FROM" keyword = "FROM"
with open(path, "r") as src: with open(path, "r") as src:
sources = [source_to_image(line) for line in src if line.strip().startswith(keyword)] # TODO lower/upper sources = [source_to_image(line) for line in src if line.strip().startswith(keyword)]
return sources return sources
def image_info(image): def image_info(image):
@ -129,7 +129,6 @@ def args_setup(description):
parser.add_argument("compose_files", nargs="+") parser.add_argument("compose_files", nargs="+")
parser.add_argument("--output", "-o") parser.add_argument("--output", "-o")
parser.add_argument("--ignore", "-i", nargs="+", default=False) parser.add_argument("--ignore", "-i", nargs="+", default=False)
parser.add_argument("--match-suffix", "-s", action="store_true")
return parser return parser
if __name__ == "__main__": if __name__ == "__main__":
@ -142,4 +141,4 @@ if __name__ == "__main__":
with open(args.output, "w") as out: with open(args.output, "w") as out:
json.dump(overview, out, indent=1, sort_keys=True) json.dump(overview, out, indent=1, sort_keys=True)
else: else:
print(json.dumps(overview, indent=1)) print(json.dumps(overview, indent=1))

View File

@ -17,7 +17,7 @@ TAG_STORE = {}
def api_call(url): def api_call(url):
result = requests.get(url) result = requests.get(url)
if not result.ok: if not result.ok:
log.error(f"{result}, {result.status_code}, {result.url}") log.error(result, result.url)
return {} return {}
data = result.json() data = result.json()
tags = {} tags = {}
@ -44,11 +44,7 @@ def replace(string, replacements):
return string return string
def compare(base, other, match_suffix=False, replacements=[("-","+"),]): def compare(base, other, replacements=[("-","+"),]):
if match_suffix and "-" in base:
suffix = base.split("-")[-1]
if not other.endswith(suffix):
return False
base = replace(base, replacements) base = replace(base, replacements)
other = replace(other, replacements) other = replace(other, replacements)
v1 = version.parse(base) v1 = version.parse(base)
@ -57,17 +53,23 @@ def compare(base, other, match_suffix=False, replacements=[("-","+"),]):
log.debug(f"{v1} < {v2}: {result}") log.debug(f"{v1} < {v2}: {result}")
return result return result
def get_new_tags(image, match_suffix=False): def get_new_tags(image):
if not ":" in image: if not ":" in image:
log.warn("using implicit latest, skip") log.warn("using implicit latest, skip")
return return
image_name, current_tag = image.split(":") image_name, current_tag = image.split(":")
if not image_name in TAG_STORE: if not image_name in TAG_STORE:
TAG_STORE[image_name] = get_tags(image_name) TAG_STORE[image_name] = get_tags(image_name)
#if current_tag in TAG_STORE[image_name]:
# first_update = TAG_STORE[image_name][current_tag]
#else:
# print("!!! FALLBACK!")
# first_update = list(TAG_STORE[image_name].values())[0]
#print(first_update)
new_tags = {} new_tags = {}
for tag in TAG_STORE[image_name]: for tag in TAG_STORE[image_name]:
log.debug("check("+str(tag)+")") log.debug("check("+str(tag)+")")
if compare(current_tag, tag, match_suffix): if compare(current_tag, tag):
log.debug("NEWER!!!") log.debug("NEWER!!!")
update = TAG_STORE[image_name][tag] update = TAG_STORE[image_name][tag]
new_tags[tag] = str(update) new_tags[tag] = str(update)

127
readme.md
View File

@ -1,127 +0,0 @@
Docker Update
=============
Show available image-updates for your docker-compose managed services. Checks docker-compose image-tags as well as connected Dockerfiles in build-sections.
Lists (possible) available updates and where the old image(-tag) is used. Optimized for use with "pinned" tags. (Use a specific minor version tag to fuse your infrastrucutre - e.g. `10.2-alpine` instead of `10-alpine`.)
Requirements
------------
* Docker
* docker-compose
* Python >=3.6
* Libraries: requirements.txt (`pip3 install -r requirements.txt` or docker ;))
Filesystem Structure
--------------------
You can call it on a single directory containing your `docker-compose.yml`.
Or you can execute it on a directory containing your service directories. These services must have a docker-compose.yml to get checked.
Example:
```
└── services
├── bitpoll.example.org
│   ├── docker
│  │  └── Dockerfile
│   └── docker-compose.yml
├── dockerui
│   └── docker-compose.yml
└── gitea
└── docker-compose.yml
```
If there are files or directories without a docker-compse.yml, it will just notify you and ignore it.
If the compose file contains a build-section, the Dockerfile is inspected, too.
Usage
----
### Docker
Modify mount of services directory. Mount your directory as `/services`.
```
docker-compose up
```
Output file: `updates.json`
### Command line
```
$ python3 show_updateable.py -h
usage: show_updateable.py [-h] [--output OUTPUT]
[--ignore IGNORE [IGNORE ...]] [--match-suffix]
compose_files [compose_files ...]
```
* output: json file for results
* ignore: ignore services (ignore is substring of service path)
* match-suffix: use only same suffixes in image labels (e.g. only -alpine images)
* compose files: service directories: see #example (multiple paths allowed)
Example Output
--------------
```
{
"postgres:10-alpine": {
"updates": {
"10.1-alpine": "2018-01-10 04:44:17.433471",
"10.2-alpine": "2018-02-19 19:43:46.911031",
"10.3-alpine": "2018-05-12 10:44:57.814207",
"10.4-alpine": "2018-08-01 14:49:09.002434",
"11-alpine": "2018-08-01 14:46:34.449579"
},
"usages": [
{
"path": "/services/bitpoll.example.org/docker-compose.yml",
"service_name": "dbbitpoll.example.org"
},
{
"path": "/services/gitea/docker-compose.yml",
"service_name": "dbgitea"
}
]
}
}
```
Advantages
----------
* No access to Docker-Socket
* No deamon
* No state
* Detect new major versions
* Report only, no uncontrolled automated actions
* tbc ...
Alternatives
------------
* https://github.com/v2tec/watchtower (seems quite dead, look for forks)
* https://github.com/pyouroboros/ouroboros
* https://engineering.salesforce.com/open-sourcing-dockerfile-image-update-6400121c1a
* https://stackoverflow.com/questions/26423515/how-to-automatically-update-your-docker-containers-if-base-images-are-updated
* tbc ...
Known Issues
------------
* Still WiP/PoC
* http/https sources are not implemented yet
* does not handle image updates without changing tags
* some images have … weird tags
* pull requests welcome

View File

@ -2,7 +2,7 @@ version: "2"
services: services:
http: http:
image: httpd:2.4.32-alpine image: httpd:alpine
# ports: # ports:
# - "80:80" # - "80:80"
volumes: volumes:
@ -12,9 +12,8 @@ services:
labels: labels:
- "traefik.enable=true" - "traefik.enable=true"
- "traefik.docker.network=traefik_net" - "traefik.docker.network=traefik_net"
- "traefik.http.frontend.rule=Host:git24.example" - "traefik.http.frontend.rule=Host:potato.kinf.wiai.uni-bamberg.de,www.potato.kinf.wiai.uni-bamberg.de,141.13.94.68,localhost,uni.clkl.de"
networks: networks:
traefik_net: traefik_net:
external: external:
name: traefik_net name: traefik_net

View File

@ -1,35 +0,0 @@
version: '2'
services:
cloud:
image: nextcloud:14.0.8-apache
# image: owncloud/server:10.0.8
restart: on-failure:5
depends_on:
- dbcloud
volumes:
- ./data:/files
- ./config:/var/www/html/config
- ./apps:/var/www/html/custom_apps
ports:
- "127.0.0.1:3020:80"
dbcloud:
image: mariadb:10.3.2
restart: on-failure:5
env_file:
- docker.env
environment:
# - MYSQL_ROOT_PASSWORD=VerySecurePassword
# - MYSQL_USER=cloud
# - MYSQL_PASSWORD=CorrectBatteryHorseStaple
# - MYSQL_DATABASE=cloud
- MYSQL_MAX_ALLOWED_PACKET=128M
- MYSQL_INNODB_LOG_FILE_SIZE=64M
- MYSQL_INNODB_LARGE_PREFIX=ON
- MYSQL_INNODB_FILE_FORMAT=Barracuda
volumes:
- ./mysql:/var/lib/mysql

View File

@ -1,22 +0,0 @@
version: "3"
services:
someservice:
build:
context: ./docker/
dockerfile: Dockerfile
ports:
- "8080:80"
restart: on-failure:5
depends_on:
- dbsomeservice
dbsomeservice:
image: mariadb:10.3.2
restart: on-failure:5
env_file:
- docker.env
volumes:
- ./mysql:/var/lib/mysql

View File

@ -1,7 +0,0 @@
FROM alpine:3.6
EXPOSE 80
RUN apk add --update python3 python3-dev git g++ nodejs-npm mariadb-dev mariadb-client-libs openldap-dev \
&& apk del nodejs-npm mariadb-dev git g++ python3-dev

View File

@ -1,21 +1,10 @@
import argparse import argparse
import json import json
import logging
import docker_compose import docker_compose
import image_tags import image_tags
log = logging
def find_updates(image_ref, usages, match_suffix=False):
try:
newer_tags = image_tags.get_new_tags(image_ref, match_suffix)
except ValueError as e:
newer_tags = e.args
return {
"updates": newer_tags,
"usages": usages,
}
def main(args): def main(args):
@ -24,24 +13,14 @@ def main(args):
for image in images: for image in images:
for tag in images[image]: for tag in images[image]:
image_ref = f"{image}:{tag}" image_ref = f"{image}:{tag}"
if image_ref in updates: try:
continue newer_tags = image_tags.get_new_tags(image_ref)
updates[image_ref] = find_updates(image_ref, images[image][tag], args.match_suffix) except ValueError as e:
for usage in images[image][tag]: newer_tags = e.args
if not "base_images" in usage: updates[image_ref] = {
continue "updates": newer_tags,
for base in usage["base_images"]: "usages": images[image][tag]
info = { }
"is_base_image": True,
"path": usage["path"],
"service_name": usage["service_name"]
}
if base in updates:
updates[base]["usages"].append(info)
else:
log.info(f"find base image updates for {base}")
updates[base] = find_updates(base, [info], args.match_suffix)
if args.output: if args.output:
with open(args.output, "w") as out: with open(args.output, "w") as out:
json.dump(updates, out, indent=1, sort_keys=True) json.dump(updates, out, indent=1, sort_keys=True)
@ -52,5 +31,5 @@ def main(args):
if __name__=="__main__": if __name__=="__main__":
parser = docker_compose.args_setup("Show updates for docker-compose style services") parser = docker_compose.args_setup("Show updates for docker-compose style services")
args = parser.parse_args() args = parser.parse_args()
print(args)
main(args) main(args)