added version checker function to glance-helper

This commit is contained in:
2026-01-15 18:20:05 +01:00
parent cf5626d5b9
commit e9b0576575
+227 -5
View File
@@ -328,13 +328,233 @@ data:
if redirect: if redirect:
return RedirectResponse(url=redirect, status_code=302) return RedirectResponse(url=redirect, status_code=302)
return {"ok": True, "date": today, "cooked": id} return {"ok": True, "date": today, "cooked": id}
# ================================
# Version Checker - Container Version Monitoring
# ================================
# Scrapes version-checker Prometheus metrics, parses versions,
# and returns properly sorted results with the NEWEST available version.
VERSION_CHECKER_URL = os.getenv("VERSION_CHECKER_URL", "http://version-checker.version-checker-system.svc.cluster.local:8080/metrics")
# Regex patterns for filtering (same as in Glance widget)
VERSION_CHECKER_EXCLUDE_IMAGES = os.getenv("VERSION_CHECKER_EXCLUDE_IMAGES",
r"(^|.*/)(busybox|redis|alpine|nginx|mariadb|mysql|valkey)$|^longhornio.*$|(^|.*/)postgres.*$|^registry\.k8s\.io/ingress-nginx/.*$"
)
def _parse_semver(version: str) -> tuple:
"""
Parse version string into comparable tuple.
Handles: v1.2.3, 1.2.3, v1.2.3-beta, 1.2.3-20251030, etc.
Returns tuple that sorts correctly: (major, minor, patch, prerelease_flag, prerelease)
"""
if not version:
return (0, 0, 0, 0, "")
# Skip sha256 digests entirely
if "sha256:" in version or version.startswith("sha"):
return (0, 0, 0, 0, version)
# Remove 'v' prefix
v = version.lstrip("v")
# Remove @sha256:... suffix if present (e.g., "1.2.3@sha256:abc...")
if "@" in v:
v = v.split("@")[0]
# Split into version and prerelease/build metadata
# Handle: 1.2.3-beta, 1.2.3-20251030, 1.2.3+build
prerelease = ""
if "-" in v:
parts = v.split("-", 1)
v = parts[0]
prerelease = parts[1]
elif "+" in v:
parts = v.split("+", 1)
v = parts[0]
prerelease = parts[1]
# Parse major.minor.patch
segments = v.split(".")
try:
major = int(segments[0]) if len(segments) > 0 and segments[0].isdigit() else 0
minor = int(segments[1]) if len(segments) > 1 and segments[1].isdigit() else 0
patch = int(segments[2]) if len(segments) > 2 and segments[2].isdigit() else 0
except (ValueError, IndexError):
major, minor, patch = 0, 0, 0
# For sorting: no prerelease > prerelease (1.2.3 > 1.2.3-beta)
# prerelease_flag: 1 = no prerelease (higher), 0 = has prerelease (lower)
prerelease_flag = 1 if not prerelease else 0
return (major, minor, patch, prerelease_flag, prerelease)
def _compare_versions(v1: str, v2: str) -> int:
"""Compare two version strings. Returns: -1 if v1<v2, 0 if equal, 1 if v1>v2"""
t1 = _parse_semver(v1)
t2 = _parse_semver(v2)
if t1 < t2:
return -1
elif t1 > t2:
return 1
return 0
def _parse_prometheus_metrics(text: str) -> list[dict]:
"""
Parse Prometheus metrics text format.
Extracts version_checker_is_latest_version metrics.
"""
results = []
pattern = re.compile(
r'version_checker_is_latest_version\{([^}]+)\}\s+(\d+(?:\.\d+)?)'
)
for match in pattern.finditer(text):
labels_str = match.group(1)
value = float(match.group(2))
# Parse labels
labels = {}
# Handle labels like: container="foo",image="bar",current_version="1.0"
label_pattern = re.compile(r'(\w+)="([^"]*)"')
for label_match in label_pattern.finditer(labels_str):
labels[label_match.group(1)] = label_match.group(2)
results.append({
"labels": labels,
"value": value # 1 = up to date, 0 = outdated
})
return results
def _fetch_version_checker_data() -> dict:
"""
Fetch and process version-checker metrics.
Returns structured data with deduplicated images and newest versions.
"""
try:
r = requests.get(VERSION_CHECKER_URL, timeout=30)
r.raise_for_status()
metrics = _parse_prometheus_metrics(r.text)
except Exception as e:
print(f"Version checker fetch error: {e}")
raise HTTPException(status_code=502, detail=f"Failed to fetch version-checker: {e}")
# Build exclude pattern
exclude_pattern = re.compile(VERSION_CHECKER_EXCLUDE_IMAGES) if VERSION_CHECKER_EXCLUDE_IMAGES else None
# Group metrics by image
# Structure: {image: {"current": str, "latest_versions": [str], "is_latest": bool}}
images: Dict[str, Dict] = {}
for metric in metrics:
labels = metric["labels"]
container_type = labels.get("container_type", "")
image = labels.get("image", "")
current = labels.get("current_version", "")
latest = labels.get("latest_version", "")
is_latest = metric["value"] == 1
# Filter: only containers (not init containers for simplicity)
if container_type != "container":
continue
# Filter: exclude patterns
if exclude_pattern and exclude_pattern.search(image):
continue
# Filter: skip sha256 versions (can't compare meaningfully)
if "sha256:" in current or "sha256:" in latest:
continue
# Initialize or update image entry
if image not in images:
images[image] = {
"current_version": current,
"latest_versions": [],
"is_latest": True
}
# Track all latest versions reported (for outdated images)
if not is_latest:
images[image]["is_latest"] = False
if latest and latest not in images[image]["latest_versions"]:
images[image]["latest_versions"].append(latest)
# Process: find the NEWEST version for each image
up_to_date = []
outdated = []
for image, data in images.items():
current = data["current_version"]
if data["is_latest"]:
up_to_date.append({
"image": image,
"current_version": current,
"latest_version": current
})
else:
# Sort versions and pick the newest
versions = data["latest_versions"]
if versions:
versions_sorted = sorted(versions, key=lambda v: _parse_semver(v), reverse=True)
newest = versions_sorted[0]
else:
newest = current
outdated.append({
"image": image,
"current_version": current,
"latest_version": newest,
"all_newer_versions": len(versions)
})
# Sort outdated by image name for consistent ordering
outdated.sort(key=lambda x: x["image"])
up_to_date.sort(key=lambda x: x["image"])
return {
"fetched_at_unix": int(time.time()),
"summary": {
"up_to_date": len(up_to_date),
"outdated": len(outdated),
"total": len(up_to_date) + len(outdated)
},
"outdated": outdated,
"up_to_date": up_to_date
}
@APP.get("/versions")
def versions_api():
"""
Returns container version status with properly sorted newest versions.
Response format optimized for Glance custom-api widget.
"""
data = _fetch_version_checker_data()
return Response(
content=json.dumps(data, ensure_ascii=False),
media_type="application/json; charset=utf-8"
)
@APP.get("/versions/outdated")
def versions_outdated():
"""Returns only outdated images (for simpler widget)."""
data = _fetch_version_checker_data()
return Response(
content=json.dumps({
"fetched_at_unix": data["fetched_at_unix"],
"summary": data["summary"],
"images": data["outdated"]
}, ensure_ascii=False),
media_type="application/json; charset=utf-8"
)
--- ---
apiVersion: apps/v1 apiVersion: apps/v1
kind: Deployment kind: Deployment
metadata: metadata:
annotations: annotations:
argocd.argoproj.io/tracking-id: glance:apps/Deployment:glance-system/glance-helper argocd.argoproj.io/tracking-id: glance:apps/Deployment:glance-system/glance-helper
deployment.kubernetes.io/revision: "9"
reloader.stakater.com/auto: "true" reloader.stakater.com/auto: "true"
name: glance-helper name: glance-helper
namespace: glance-system namespace: glance-system
@@ -349,7 +569,6 @@ spec:
type: Recreate type: Recreate
template: template:
metadata: metadata:
creationTimestamp: null
labels: labels:
app: glance-helper app: glance-helper
app.kubernetes.io/name: glance-helper app.kubernetes.io/name: glance-helper
@@ -368,8 +587,6 @@ spec:
- /bin/sh - /bin/sh
- -lc - -lc
env: env:
- name: STAKATER_GLANCE_HELPER_APP_CONFIGMAP
value: c6e34df8429997db4bd9c1b119c5a6f1e2337ecb
- name: IDOKEP_URL - name: IDOKEP_URL
value: https://www.idokep.hu/idojaras/Budapest%20VII.%20ker value: https://www.idokep.hu/idojaras/Budapest%20VII.%20ker
- name: PLACE_NAME - name: PLACE_NAME
@@ -388,6 +605,11 @@ spec:
value: oplQqnLnJK2vErRVYJpvVUcSDBOSbCHZSbsYY2bwSifgTMfT value: oplQqnLnJK2vErRVYJpvVUcSDBOSbCHZSbsYY2bwSifgTMfT
- name: TZ - name: TZ
value: Europe/Budapest value: Europe/Budapest
# Version Checker configuration
- name: VERSION_CHECKER_URL
value: http://version-checker.version-checker-system.svc.cluster.local:8080/metrics
- name: VERSION_CHECKER_EXCLUDE_IMAGES
value: "(^|.*/)(busybox|redis|alpine|nginx|mariadb|mysql|valkey)$|^longhornio.*$|(^|.*/)postgres.*$|^registry\\.k8s\\.io/ingress-nginx/.*$"
image: python:3.12-bookworm image: python:3.12-bookworm
imagePullPolicy: IfNotPresent imagePullPolicy: IfNotPresent
name: glance-helper name: glance-helper
@@ -468,4 +690,4 @@ spec:
tls: tls:
- hosts: - hosts:
- glance-helper.dooplex.hu - glance-helper.dooplex.hu
secretName: glance-helper-tls secretName: glance-helper-tls