diff --git a/apps/registry/retention-script.yaml b/apps/registry/retention-script.yaml index 276817f..603383a 100644 --- a/apps/registry/retention-script.yaml +++ b/apps/registry/retention-script.yaml @@ -9,22 +9,51 @@ data: REG='https://registry.nxtgauge.com' CFG='/auth/.dockerconfigjson' PATTERN=re.compile(r'^[0-9a-f]{40}$') - + + # Base images that MUST NEVER be deleted, even if their names start with + # nxtgauge- in the future. These are the FROM lines in our Dockerfiles + # (alpine for rust, node variants for frontend/admin, etc.). If any of + # these are missing the entire build pipeline breaks. + BASE_IMAGES = { + 'alpine', + 'node', + 'rust', + 'busybox', + 'golang', + 'nginx', + 'postgres', + 'redis', + } + # Project-image prefix that we DO prune. Anything outside this is sacred. + PROJECT_PREFIX = 'nxtgauge-' + with open(CFG,'r') as f: dcfg=json.load(f) auth=dcfg['auths']['registry.nxtgauge.com']['auth'] HEAD={'Authorization': f'Basic {auth}'} - + def req(url, headers=None, method='GET'): h=dict(HEAD) if headers: h.update(headers) r=urllib.request.Request(url, headers=h, method=method) with urllib.request.urlopen(r, timeout=30) as resp: return resp.status, dict(resp.headers), resp.read() - + _, _, body = req(f'{REG}/v2/_catalog?n=1000') - repos=[r for r in json.loads(body.decode()).get('repositories',[]) if r.startswith('nxtgauge-')] - + all_repos=json.loads(body.decode()).get('repositories',[]) + + # EXPLICIT SAFETY: only consider repos that match the project prefix. + # This double-belt-and-suspenders: base images (alpine/node/rust) are + # also in BASE_IMAGES as a fallback in case the prefix is ever changed. + repos=[r for r in all_repos if r.startswith(PROJECT_PREFIX) and r not in BASE_IMAGES] + + # Sanity check: log if any base image is missing + missing_base = [b for b in BASE_IMAGES if b in all_repos or True] # always present + present = set(all_repos) + for b in BASE_IMAGES: + if b not in present: + print(f'[WARN] base image {b} not in registry catalog - re-push required!') + deleted=0 for repo in sorted(repos): try: @@ -33,12 +62,12 @@ data: except Exception as e: print(f'[{repo}] tags/list failed: {e}') continue - + sha=[t for t in tags if PATTERN.match(t)] if len(sha)<=1: print(f'[{repo}] sha={len(sha)} no prune') continue - + rows=[] for t in sha: created='1970-01-01T00:00:00Z' @@ -54,7 +83,7 @@ data: except Exception: created='9999-12-31T23:59:59Z' rows.append((created, t, digest)) - + rows.sort(key=lambda x: x[0], reverse=True) KEEP_N=3 # keep last 3 SHA builds (was 1; bumped to prevent auth-blast-radius wipeouts) keep_set=set(t for _, t, _ in rows[:KEEP_N]) @@ -73,5 +102,5 @@ data: print(f' delete failed {repo}:{t} code={e.code}') except Exception as e: print(f' delete failed {repo}:{t} err={e}') - + print(f'deleted_manifests={deleted}')