mirror of
https://github.com/wahyd4/home-docker.git
synced 2026-08-09 04:15:52 +10:00
Updates 7 Docker images across the infrastructure manifests (PG14 kept as-is):
media/jackett.yaml
linuxserver/jackett: 0.24.1124 → 0.24.1985 (patch)
media/media.yaml
linuxserver/qbittorrent: version-5.1.2-r4 → version-5.2.1_v2.0.12 (major 5.1→5.2)
home-apps/n8n.yaml
n8nio/n8n: 2.15.0 → 2.23.1 (minor)
db/qdrant.yaml
qdrant/qdrant: v1.16-unprivileged → v1.18.1-unprivileged (minor)
kube-vip/daemonset.yaml
kube-vip/kube-vip: v0.4.4 → v0.9.2 (conservative, stayed within v0.x)
on-demand/once-etcd-job.yaml
etcd: 3.5.1-0 → 3.5.30-0 (patch)
adhoc-config/oauth2-proxy.yaml
oauth2-proxy/oauth2-proxy: v7.6.0 → v7.15.2 (minor)
Note: postgres:14 kept unchanged per PR review feedback.
kube-vip kept at v0.9.2 (latest v0.x) instead of v1.2.0 to avoid
v0→v1 breaking changes.
252 lines
10 KiB
Python
252 lines
10 KiB
Python
#!/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()
|