diff --git a/glance-system/glance-helper.yaml b/glance-system/glance-helper.yaml index 998c28d..a080a87 100644 --- a/glance-system/glance-helper.yaml +++ b/glance-system/glance-helper.yaml @@ -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 v1v2""" + 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 \ No newline at end of file