#!/usr/bin/env python3 """Verify a container image with disposable networking, storage and notes.""" import argparse import contextlib import json from pathlib import Path import secrets import subprocess import time import urllib.error import urllib.parse import urllib.request parser = argparse.ArgumentParser(description=__doc__) parser.add_argument('image') parser.add_argument('--client-container', help='CI job container to connect to the test network') parser.add_argument('--resource-label', help='Label temporary resources for cancellation cleanup') args = parser.parse_args() version = Path('VERSION').read_text().strip() name = 'rawmark-ci-' + secrets.token_hex(8) labels = ('--label', args.resource_label) if args.resource_label else () def docker(*command): return subprocess.check_output(['docker', *command], text=True).strip() config = json.loads(docker('image', 'inspect', args.image))[0]['Config'] assert config['User'].split(':')[0] not in ('', '0', 'root'), 'Image must run as non-root' assert config['Healthcheck']['Test'] == ['CMD', '/rawmark', 'healthcheck'] assert docker('run', '--rm', '--network', 'none', *labels, args.image, '--version') == f'RawMark {version}' with contextlib.ExitStack() as cleanup: docker('network', 'create', '--internal', *labels, name) cleanup.callback(docker, 'network', 'rm', name) docker('volume', 'create', *labels, name) cleanup.callback(docker, 'volume', 'rm', name) if args.client_container: docker('network', 'connect', name, args.client_container) cleanup.callback(docker, 'network', 'disconnect', name, args.client_container) create = ('create', '--name', name, '--network', name, *labels, '--mount', f'type=volume,source={name},target=/data', '--cpus', '1', '--memory', '512m', '--health-interval', '1s', '--health-start-period', '1s', args.image) docker(*create) cleanup.callback(docker, 'rm', '--force', '--volumes', name) docker('start', name) info = json.loads(docker('inspect', name))[0] host = name if args.client_container else info['NetworkSettings']['Networks'][name]['IPAddress'] origin = f'http://{host}:8080' opener = urllib.request.build_opener(urllib.request.ProxyHandler({})) def request(path, *, method='GET', body=None, token=None, status=200, raw=False): headers = {'Accept-Language': 'en'} if token: headers['Authorization'] = 'Bearer ' + token if body is not None and not isinstance(body, bytes): headers['Content-Type'] = 'application/json' body = json.dumps(body).encode() req = urllib.request.Request(origin + path, data=body, headers=headers, method=method) try: response = opener.open(req, timeout=5) except urllib.error.HTTPError as error: response = error with response: assert response.status == status, (path, response.status, response.read()) if raw: return response.read() result = json.load(response) assert (result['code'] == 0) == (status < 400), result return result['data'] def wait_healthy(): for _ in range(60): state = json.loads(docker('inspect', name))[0]['State'] if state['Status'] != 'running' or state['Health']['Status'] == 'unhealthy': raise RuntimeError(f'Container failed: {state["Status"]}, {state["Health"]["Status"]}') if state['Health']['Status'] == 'healthy': docker('exec', name, '/rawmark', 'healthcheck') request('/api/v1/health') return time.sleep(1) raise TimeoutError('Container did not become healthy') try: wait_healthy() assert b'