#!/usr/bin/env python3 """Script to query Docker registries for latest versions.""" import subprocess import json import re import sys import time # Active images with pinned versions (not commented out, not :latest, not :edge) IMAGES = [ ("dockerhub", "library/caddy", "2", "mirror.gcr.io/caddy:2"), ("dockerhub", "linuxserver/jackett", "0.24.1124", "mirror.gcr.io/linuxserver/jackett:0.24.1124"), ("dockerhub", "linuxserver/qbittorrent", "version-5.1.2-r4", "mirror.gcr.io/linuxserver/qbittorrent:version-5.1.2-r4"), ("dockerhub", "n8nio/n8n", "2.15.0", "mirror.gcr.io/n8nio/n8n:2.15.0"), ("dockerhub", "library/postgres", "14", "mirror.gcr.io/postgres:14"), ("dockerhub", "qdrant/qdrant", "v1.16-unprivileged", "mirror.gcr.io/qdrant/qdrant:v1.16-unprivileged"), ("ghcr", "kube-vip/kube-vip", "v0.4.4", "ghcr.io/kube-vip/kube-vip:v0.4.4"), ("quay", "oauth2-proxy/oauth2-proxy", "v7.6.0", "quay.io/oauth2-proxy/oauth2-proxy:v7.6.0"), ] def safe_version_sort_key(tag_str): parts = tag_str.lstrip('v').split('.') nums = [] for p in parts: try: nums.append(int(re.match(r'(\d+)', p).group(1))) except (ValueError, AttributeError, TypeError): nums.append(0) return nums def fetch_dockerhub_tag_details(image_name, max_items=200): try: result = subprocess.run( ["curl", "-s", "-L", "-f", f"https://hub.docker.com/v2/repositories/{image_name}/tags?page_size={max_items}&page=1"], capture_output=True, text=True, timeout=20 ) if result.returncode != 0: return None, f"curl failed: {result.stderr[:300]}" data = json.loads(result.stdout) tags_detail = [] for tag_data in data.get("results", []): tag_name = tag_data.get("name", "") if tag_name and not tag_name.startswith("sha256:"): tags_detail.append({ "name": tag_name, "last_updated": tag_data.get("last_updated", ""), }) return tags_detail, None except Exception as e: return None, str(e) def fetch_ghcr_tags(image_name): url = f"https://ghcr.io/v2/{image_name}/tags/list" result = subprocess.run( ["curl", "-s", "-L", "-f", url], capture_output=True, text=True, timeout=20 ) if result.returncode != 0: return None, f"GHCR API failed: {result.stderr[:300]}" try: data = json.loads(result.stdout) tags = data.get("tags", []) return [t for t in tags if not t.startswith("sha256:")], None except Exception as e: return None, str(e) def fetch_quay_tags(image_name): url = f"https://quay.io/api/v1/repository/{image_name}?includeTags=true&limit=100" result = subprocess.run( ["curl", "-s", "-L", "-f", url], capture_output=True, text=True, timeout=20 ) if result.returncode != 0: return None, f"Quay API failed: {result.stderr[:300]}" try: data = json.loads(result.stdout) tags = list(data.get("tags", {}).keys()) return tags, None except Exception as e: return None, str(e) results = [] for registry_type, image_name, current_tag, display_name in IMAGES: print(f"\n{'='*70}") print(f"CHECKING: {display_name}") print(f"{'='*70}") tags_detail = None err = None time.sleep(0.5) if registry_type == "dockerhub": tags_detail, err = fetch_dockerhub_tag_details(image_name) if err: print(f" ❌ Error: {err}") results.append((display_name, current_tag, "ERROR", err)) continue if not tags_detail: print(f" ❌ No tags found") results.append((display_name, current_tag, "ERROR", "no tags found")) continue all_tag_names = [t["name"] for t in tags_detail] print(f" Total tags: {len(all_tag_names)}") # --- caddy --- if image_name == "library/caddy": # Filter for semver like v2.8.x, v2.9.x etc. specific = [t for t in all_tag_names if re.match(r'^v?\d+\.\d+\.\d+$', t)] specific.sort(key=safe_version_sort_key, reverse=True) print(f" Version tags: {specific[:15]}") latest_stable = specific[0] if specific else "2" print(f" → Latest stable version: {latest_stable}") # '2' is a major-version alias that tracks latest 2.x results.append((display_name, current_tag, latest_stable, f"Tag '2' tracks latest 2.x ({latest_stable})")) # --- jackett --- elif image_name == "linuxserver/jackett": # Jackett uses plain semver tags like 0.24.1124 jackett_stable = [t for t in all_tag_names if re.match(r'^\d+\.\d+\.\d+$', t)] jackett_stable.sort(key=safe_version_sort_key, reverse=True) print(f" Stable tags (latest first): {jackett_stable[:10]}") latest_stable = jackett_stable[0] if jackett_stable else "unknown" print(f" → Latest stable version: {latest_stable}") results.append((display_name, current_tag, latest_stable, "")) # --- qbittorrent --- elif image_name == "linuxserver/qbittorrent": qbit_tags = [t for t in all_tag_names if re.match(r'version-', t)] print(f" 'version-*' tags: {qbit_tags[:15]}") qbit_tags.sort(key=safe_version_sort_key, reverse=True) latest_stable = qbit_tags[0] if qbit_tags else "unknown" print(f" → Latest 'version-*' tag: {latest_stable}") results.append((display_name, current_tag, latest_stable, "")) # --- n8n --- elif image_name == "n8nio/n8n": n8n_tags = [t for t in all_tag_names if re.match(r'^\d+\.\d+\.\d+$', t) and not re.search(r'(alpha|beta|rc|dev|nightly)', t, re.I)] n8n_tags.sort(key=safe_version_sort_key, reverse=True) print(f" Stable tags (latest first): {n8n_tags[:10]}") latest_stable = n8n_tags[0] if n8n_tags else "unknown" print(f" → Latest stable version: {latest_stable}") results.append((display_name, current_tag, latest_stable, "")) # --- postgres --- elif image_name == "library/postgres": major_tags = [t for t in all_tag_names if re.match(r'^\d+$', t) and int(t) >= 10] major_tags.sort(key=int, reverse=True) print(f" Major version tags: {major_tags[:10]}") latest_stable = major_tags[0] if major_tags else "unknown" print(f" → Latest major version: {latest_stable}") results.append((display_name, current_tag, latest_stable, "")) # --- qdrant --- elif image_name == "qdrant/qdrant": # Unprivileged tags unpriv = [t for t in all_tag_names if 'unprivileged' in t.lower()] unpriv.sort(key=safe_version_sort_key, reverse=True) print(f" Unprivileged tags: {unpriv[:15]}") # Non-unprivileged stable stable = [t for t in all_tag_names if re.match(r'^v?\d+\.\d+', t) and 'unprivileged' not in t.lower() and not re.search(r'(alpha|beta|rc|dev|nightly)', t, re.I)] stable.sort(key=safe_version_sort_key, reverse=True) print(f" Stable version tags: {stable[:10]}") latest_stable = unpriv[0] if unpriv else "unknown" print(f" → Latest unprivileged: {latest_stable}") results.append((display_name, current_tag, latest_stable, "")) else: stable = [t for t in all_tag_names if re.match(r'^v?\d+\.\d+', t) and not re.search(r'(alpha|beta|rc|dev|nightly)', t, re.I)] stable.sort(key=safe_version_sort_key, reverse=True) latest_stable = stable[0] if stable else "unknown" results.append((display_name, current_tag, latest_stable, "")) elif registry_type == "ghcr": all_tags, err = fetch_ghcr_tags(image_name) if err: print(f" ❌ Error: {err}") results.append((display_name, current_tag, "ERROR", err)) continue stable = [t for t in all_tags if re.match(r'^v?\d+\.\d+', t) and not re.search(r'(alpha|beta|rc|dev|nightly)', t, re.I)] stable.sort(key=safe_version_sort_key, reverse=True) latest_stable = stable[0] if stable else all_tags[0] if all_tags else "unknown" print(f" Tags: {all_tags[:20]}") print(f" → Latest stable version: {latest_stable}") results.append((display_name, current_tag, latest_stable, "")) elif registry_type == "quay": all_tags, err = fetch_quay_tags(image_name) if err: print(f" ❌ Error: {err}") results.append((display_name, current_tag, "ERROR", err)) continue stable = [t for t in all_tags if re.match(r'^v?\d+\.\d+', t) and not re.search(r'(alpha|beta|rc|dev|nightly)', t, re.I)] stable.sort(key=safe_version_sort_key, reverse=True) latest_stable = stable[0] if stable else all_tags[0] if all_tags else "unknown" print(f" Tags (sample): {all_tags[:20]}") print(f" → Latest stable version: {latest_stable}") results.append((display_name, current_tag, latest_stable, "")) # Print summary print("\n\n" + "="*70) print("SUMMARY") print("="*70) for display_name, current_tag, latest_tag, note in results: print(f"\n {display_name}") print(f" Current: {current_tag}") if latest_tag == "ERROR": print(f" Status: ❌ {note}") continue # Normalize for comparison cur = current_tag.lstrip('v').lower() lat = latest_tag.lstrip('v').lower() if cur == lat: print(f" Latest: {latest_tag} ✓") else: print(f" Latest: {latest_tag}") print(f" Status: ⚠ UPDATE AVAILABLE") print()