From 54207b4f5158a8bd618ae0a6cc2edd1533f03d7f Mon Sep 17 00:00:00 2001 From: Ian Fijolek Date: Tue, 1 Dec 2020 17:04:35 -0800 Subject: [PATCH] Initial Python version --- .gitignore | 2 + Dockerfile | 10 +++ README.md | 7 ++- main.py | 144 +++++++++++++++++++++++++++++++++++++++++++ requirements-dev.txt | 5 ++ requirements.txt | 2 + 6 files changed, 169 insertions(+), 1 deletion(-) create mode 100644 Dockerfile create mode 100644 main.py create mode 100644 requirements-dev.txt create mode 100644 requirements.txt diff --git a/.gitignore b/.gitignore index f4d432a..b5c145e 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,5 @@ # Dependency directories (remove the comment below to include it) # vendor/ +__pycache__/ +venv/ diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..7badf91 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,10 @@ +FROM python:3 + +RUN mkdir -p /app +WORKDIR /app + +COPY ./requirements.txt /app/ +RUN pip install -r ./requirements.txt +COPY . /app + +CMD ["python", "./main.py"] diff --git a/README.md b/README.md index 8576e93..e52e9fe 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,8 @@ # docker-check-version-updates -Checks current running containers for newer tags according to semver \ No newline at end of file +Checks current running containers for newer tags according to semver + +Usage: + + python -m venv venv + ./venv/bin/python main.py diff --git a/main.py b/main.py new file mode 100644 index 0000000..7935f96 --- /dev/null +++ b/main.py @@ -0,0 +1,144 @@ +""" +Checks to see if newer tagged versions of running images exist + +When a newer tag based on semver is found, the tags will be printed and the script will exit with +a non-zero code. +""" +from dataclasses import dataclass +from typing import Generator +from typing import List + +import docker # type: ignore +import requests + + +class NotComparableException(ValueError): + pass + + +@dataclass +class ImageTag(object): + image_tag: str + image: str + full_tag: str + version: str + tag_desc: str + version_parts: List[int] + + @classmethod + def from_str(cls, image_tag): + image, _, full_tag = image_tag.partition(":") + version, _, tag_desc = full_tag.partition("-") + # Remove leading v + if version[0].lower() == "v": + version = version[1:] + try: + version_parts = [int(p) for p in version.split(".")] + except ValueError: + version_parts = [] + return ImageTag( + image_tag=image_tag, + image=image, + full_tag=full_tag, + version=version, + tag_desc=tag_desc, + version_parts=version_parts, + ) + + def is_same_image(self, other): + return self.image == other.image + + def is_same_type(self, other): + return self.tag_desc == other.tag_desc + + def is_same_grain(self, other): + return len(self.version_parts) == len(other.version_parts) + + def is_comparable(self, other): + return all( + ( + self.is_same_image(other), + self.is_same_type(other), + self.is_same_grain(other), + ) + ) + + def __eq__(self, other): + if not self.is_comparable(other): + raise NotComparableException() + + return self.version_parts == other.version_parts + + def __lt__(self, other): + if not self.is_comparable(other): + raise NotComparableException() + + for s, o in zip(self.version_parts, other.version_parts): + if s < o: + return True + elif s > o: + return False + + return False + + def is_newer_than(self, other): + return self.is_comparable(other) and self > other + + +def get_all_tags(image_name: str) -> Generator[ImageTag, None, None]: + """Generates all tags for a given image""" + if "/" not in image_name: + image_name = f"library/{image_name}" + url = "https://registry.hub.docker.com/v2/repositories/{}/tags".format( + image_name, + ) + page_count = 0 + max_pages = 1000 + while url and page_count <= max_pages: + data = requests.get(url).json() + for tag in data["results"]: + try: + yield ImageTag.from_str(f"{image_name}:{tag['name']}") + except ValueError: + pass + + url = data["next"] + page_count += 1 + + +def generate_message(current: ImageTag, newer_tags: List[ImageTag]) -> str: + if not current.version_parts: + return f"[{current.image_tag}] No numeric version recognized" + if not newer_tags: + return f"[{current.image_tag}] No newer tags found for image tag" + else: + newer_tags = list(reversed(sorted(newer_tags))) + tags_list = ", ".join(tag.version for tag in newer_tags) + latest_tag = newer_tags[0].image_tag + return f"[{current.image_tag}] New versions found {tags_list}. Recommended update to {latest_tag}" + + +def run() -> int: + client = docker.from_env() + running_images = {container.image.tags[0] + for container in client.containers.list()} + + has_update = False + for image_name in running_images: + current = ImageTag.from_str(image_name) + + newer_tags: List[ImageTag] = [] + if current.version_parts: + newer_tags = [ + tag for tag in get_all_tags(current.image) if tag.is_newer_than(current) + ] + has_update |= bool(newer_tags) + print(generate_message(current, newer_tags)) + + if has_update: + return 1 + return 0 + + +if __name__ == "__main__": + exit(run()) diff --git a/requirements-dev.txt b/requirements-dev.txt new file mode 100644 index 0000000..56aaa53 --- /dev/null +++ b/requirements-dev.txt @@ -0,0 +1,5 @@ +pyls +pyls-black +ipython +ipdb +mypy diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..2a863ca --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +requests +docker