added version checker function to glance-helper
This commit is contained in:
@@ -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
|
||||||
Reference in New Issue
Block a user