Compare commits
No commits in common. "master" and "master" have entirely different histories.
3
.gitignore
vendored
3
.gitignore
vendored
@ -1,3 +0,0 @@
|
|||||||
__pycache__/
|
|
||||||
updates.json
|
|
||||||
|
|
||||||
@ -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
|
||||||
@ -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'
|
|
||||||
@ -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))
|
||||||
@ -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
127
readme.md
@ -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
|
|
||||||
|
|
||||||
@ -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
|
||||||
|
|
||||||
|
|||||||
@ -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
|
|
||||||
|
|
||||||
|
|
||||||
@ -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
|
|
||||||
|
|
||||||
@ -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
|
|
||||||
|
|
||||||
|
|
||||||
@ -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)
|
||||||
|
|
||||||
Loading…
x
Reference in New Issue
Block a user