Files
felhom-controller/controller/scripts/check-infra-pins.py
T

85 lines
3.0 KiB
Python
Executable File

#!/usr/bin/env python3
"""check-infra-pins.py — the MONTHLY re-test's infrastructure half (R-838, 2026-10-04).
The box's three built-in containers (traefik, cloudflared, filebrowser) are pinned in internal/infra/infra.go and are
NOT catalog templates, so retest-floating.py never sees them. Before R-838 cloudflared sat four months behind and
nothing noticed. This script prints, for each pin, the newest upstream release in the SAME channel (traefik v3.x,
cloudflared YYYY.M.P, filebrowser N.N.N-stable — never a beta) and says BEHIND when the pin is older.
It REPORTS; it changes nothing. A raise is a controller release (edit the constant, read the release notes between the
two versions for breaking changes, prove it on 9202 then both demo boxes: the runbook's "Infrastructure pins" section).
Network: Docker Hub's public tag API only. Exit 0 = all current, 1 = at least one BEHIND, 2 = could not check.
Run: python3 scripts/check-infra-pins.py
"""
import json
import os
import re
import sys
import urllib.request
HERE = os.path.dirname(os.path.abspath(__file__))
INFRA = os.path.join(HERE, "..", "internal", "infra", "infra.go")
CHANNELS = {
"TraefikImage": ("library/traefik", re.compile(r"^v3\.(\d+)\.(\d+)$")),
"CloudflaredImage": ("cloudflare/cloudflared", re.compile(r"^(\d{4})\.(\d+)\.(\d+)$")),
"FileBrowserImage": ("gtstef/filebrowser", re.compile(r"^(\d+)\.(\d+)\.(\d+)-stable$")),
}
def pins():
src = open(INFRA).read()
out = {}
for const in CHANNELS:
m = re.search(r'\b%s\s*=\s*"([^"]+)"' % const, src)
if not m:
raise SystemExit(f"check-infra-pins: {const} not found in {INFRA}")
out[const] = m.group(1)
return out
def tags(repo):
url = f"https://hub.docker.com/v2/repositories/{repo}/tags?page_size=100&ordering=last_updated"
names = []
for _ in range(3): # three pages are plenty for "the newest in a channel"
with urllib.request.urlopen(url, timeout=30) as r:
d = json.load(r)
names += [t["name"] for t in d.get("results", [])]
url = d.get("next")
if not url:
break
return names
def key(m):
return tuple(int(x) for x in m.groups())
def main():
rc = 0
for const, image in pins().items():
repo, rx = CHANNELS[const]
tag = image.split(":", 1)[1]
cur = rx.match(tag)
try:
cands = [m for m in (rx.match(t) for t in tags(repo)) if m]
except Exception as e: # noqa: BLE001 — a report, not a gate
print(f"{const:17} {image:40} could not check: {e}")
rc = max(rc, 2)
continue
if not cands or not cur:
print(f"{const:17} {image:40} no comparable tag found")
rc = max(rc, 2)
continue
newest = max(cands, key=key)
state = "current" if key(newest) <= key(cur) else "BEHIND"
if state == "BEHIND":
rc = max(rc, 1)
print(f"{const:17} {image:40} newest {newest.string:16} {state}")
return rc
if __name__ == "__main__":
sys.exit(main())