added version checker function to glance-helper
This commit is contained in:
@@ -328,13 +328,233 @@ data:
|
||||
if redirect:
|
||||
return RedirectResponse(url=redirect, status_code=302)
|
||||
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
|
||||
kind: Deployment
|
||||
metadata:
|
||||
annotations:
|
||||
argocd.argoproj.io/tracking-id: glance:apps/Deployment:glance-system/glance-helper
|
||||
deployment.kubernetes.io/revision: "9"
|
||||
reloader.stakater.com/auto: "true"
|
||||
name: glance-helper
|
||||
namespace: glance-system
|
||||
@@ -349,7 +569,6 @@ spec:
|
||||
type: Recreate
|
||||
template:
|
||||
metadata:
|
||||
creationTimestamp: null
|
||||
labels:
|
||||
app: glance-helper
|
||||
app.kubernetes.io/name: glance-helper
|
||||
@@ -368,8 +587,6 @@ spec:
|
||||
- /bin/sh
|
||||
- -lc
|
||||
env:
|
||||
- name: STAKATER_GLANCE_HELPER_APP_CONFIGMAP
|
||||
value: c6e34df8429997db4bd9c1b119c5a6f1e2337ecb
|
||||
- name: IDOKEP_URL
|
||||
value: https://www.idokep.hu/idojaras/Budapest%20VII.%20ker
|
||||
- name: PLACE_NAME
|
||||
@@ -388,6 +605,11 @@ spec:
|
||||
value: oplQqnLnJK2vErRVYJpvVUcSDBOSbCHZSbsYY2bwSifgTMfT
|
||||
- name: TZ
|
||||
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
|
||||
imagePullPolicy: IfNotPresent
|
||||
name: glance-helper
|
||||
@@ -468,4 +690,4 @@ spec:
|
||||
tls:
|
||||
- hosts:
|
||||
- glance-helper.dooplex.hu
|
||||
secretName: glance-helper-tls
|
||||
secretName: glance-helper-tls
|
||||
Reference in New Issue
Block a user