#!/usr/bin/env python3 """Resolve stable upstream releases on every build; pin their registry digests.""" import concurrent.futures import json import os from pathlib import Path import re import subprocess import urllib.request ROOT = Path(__file__).resolve().parents[1] STATE = ROOT / '.local' / 'dev' def get(url): with urllib.request.urlopen(url, timeout=45) as response: return json.load(response) def image(repo, alias): url = f'https://hub.docker.com/v2/repositories/{repo}/tags' selected = get(f'{url}/{alias}') digest = selected['digest'] if not re.fullmatch(r'sha256:[a-f0-9]{64}', digest): raise ValueError('Registry returned an invalid digest') # Find the exact numeric tag represented by the maintained stable alias. candidates = get(f'{url}?page_size=100')['results'] names = [x['name'] for x in candidates if x.get('digest') == digest and re.fullmatch(r'\d+\.\d+(?:\.\d+)?(?:-alpine)?', x['name'])] if not names: raise ValueError(f'No stable version tag for {repo}:{alias}') tag = max(names, key=lambda x: tuple(map(int, x.split('-')[0].split('.')))) return f'{repo}:{tag}@{digest}' def flutter(): commit = get('https://api.github.com/repos/flutter/flutter/commits/stable')['sha'] tags = subprocess.check_output(['git', 'ls-remote', '--tags', 'https://github.com/flutter/flutter.git'], text=True) names = [line.split('refs/tags/')[1] for line in tags.splitlines() if line.startswith(commit + '\trefs/tags/') and re.fullmatch(r'\d+\.\d+\.\d+', line.split('refs/tags/')[1])] if not names: raise ValueError('Flutter stable does not point at a stable release tag') return max(names, key=lambda x: tuple(map(int, x.split('.')))), commit def main(): go = get('https://go.dev/dl/?mode=json')[0] if not go['stable'] or not re.fullmatch(r'go\d+\.\d+\.\d+', go['version']): raise ValueError('Go release is not stable') specs = {'VAULT_IMAGE': ('hashicorp/vault', 'latest'), 'MONGO_IMAGE': ('library/mongo', 'latest'), 'NATS_IMAGE': ('library/nats', 'latest'), 'NGINX_IMAGE': ('library/nginx', 'stable-alpine'), 'GO_IMAGE': ('library/golang', go['version'][2:] + '-alpine'), 'UBUNTU_IMAGE': ('library/ubuntu', 'latest')} with concurrent.futures.ThreadPoolExecutor() as pool: futures = {key: pool.submit(image, *spec) for key, spec in specs.items()} refs = {key: future.result() for key, future in futures.items()} refs['FLUTTER_VERSION'], refs['FLUTTER_COMMIT'] = flutter() STATE.mkdir(parents=True, exist_ok=True, mode=0o700) target = STATE / 'images.env' temporary = target.with_suffix('.tmp') temporary.write_text(''.join(f'{key}={value}\n' for key, value in refs.items())) os.replace(temporary, target) for key, value in refs.items(): print(f'{key}={value}') if __name__ == '__main__': main()