232 lines
14 KiB
Python
232 lines
14 KiB
Python
"""Explicit isolated MariaDB browser lanes; self-check never touches a server."""
|
|
import argparse
|
|
import base64
|
|
import datetime
|
|
import hashlib
|
|
import json
|
|
import os
|
|
from pathlib import Path
|
|
import re
|
|
import socket
|
|
import subprocess
|
|
import sys
|
|
import time
|
|
import uuid
|
|
|
|
ROOT = Path(__file__).resolve().parents[4]
|
|
OUT = ROOT / '.artifacts/p9-requal-01'
|
|
UI = ROOT / 'job-tracker-ui'
|
|
CLIENT = ROOT / '.artifacts/mariadb/runtime/mariadb-12.2.2-winx64/bin/mariadb.exe'
|
|
CONTAINER = 'jobtracker-it-p9-requal-mariadb-test-1'
|
|
IMAGE = 'mariadb:12.2.2@sha256:42a27baaba3e8f3f0159d16381d5a7156661aa91dcc65f663cabb6a6f9503c00'
|
|
CONFIGS = {
|
|
'mariadb-browser': ('playwright.mariadb.config.ts', 1),
|
|
'mariadb-observations': ('../docs/redesign/phase-9/p9-requal-01/observations/playwright.config.ts', 2),
|
|
'performance': ('../docs/redesign/phase-9/p9-requal-01/performance/playwright.config.ts', 1),
|
|
}
|
|
|
|
def utc():
|
|
return datetime.datetime.now(datetime.timezone.utc).isoformat()
|
|
|
|
def scrub(value):
|
|
if isinstance(value, dict):
|
|
return {k: '[REDACTED]' if re.search(r'password|connectionstring', k, re.I) else scrub(v) for k, v in value.items()}
|
|
if isinstance(value, list): return [scrub(v) for v in value]
|
|
if not isinstance(value, str): return value
|
|
value = re.sub(r'Server=127\.0\.0\.1;Port=33308;Database=jt_it_[a-f0-9]{32};User ID=root;Password=[^;\r\n]+;SslMode=None;?', '[REDACTED CONNECTION]', value)
|
|
for secret in ('jt-integration-only', 'E2ePassword123!'):
|
|
value = value.replace(secret, '[REDACTED]')
|
|
return re.sub(r'(Password\s*=\s*)[^;\s]+', r'\1[REDACTED]', value, flags=re.I)
|
|
|
|
def write(path, value):
|
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
path.write_text(json.dumps(scrub(value), indent=2) + '\n', encoding='utf-8')
|
|
|
|
def schema_sql(schema, drop=False):
|
|
if not re.fullmatch(r'jt_it_[a-f0-9]{32}', schema): raise ValueError('Invalid generated schema')
|
|
return f'DROP DATABASE `{schema}`;' if drop else f'CREATE DATABASE `{schema}` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;'
|
|
|
|
def ownership(data):
|
|
labels = data['Config'].get('Labels', {})
|
|
ports = data['NetworkSettings']['Ports'].get('3306/tcp', [])
|
|
if (data['Name'] != '/' + CONTAINER or not data['State']['Running'] or data['Config']['Image'] != IMAGE
|
|
or labels.get('com.docker.compose.project') != 'jobtracker-it-p9-requal'
|
|
or labels.get('com.docker.compose.service') != 'mariadb-test'
|
|
or ports != [{'HostIp': '127.0.0.1', 'HostPort': '33308'}]
|
|
or data['HostConfig'].get('Privileged') or data['HostConfig'].get('NetworkMode') == 'host'
|
|
or any(mount.get('Destination') == '/var/lib/mysql' and mount.get('Type') != 'tmpfs' for mount in data.get('Mounts', []))
|
|
or '/var/lib/mysql' not in data['HostConfig'].get('Tmpfs', {})):
|
|
raise ValueError('Exact task-owned pinned loopback disposable container proof required')
|
|
return {'name': CONTAINER, 'id': data['Id'], 'imageReference': IMAGE, 'imageId': data['Image'],
|
|
'project': labels['com.docker.compose.project'], 'service': 'mariadb-test', 'port': 33308,
|
|
'bindHost': '127.0.0.1', 'disposableData': 'tmpfs', 'privileged': False}
|
|
|
|
def cases(report):
|
|
result = []
|
|
def walk(suite):
|
|
for spec in suite.get('specs', []):
|
|
for test in spec.get('tests', []): result.append(test)
|
|
for child in suite.get('suites', []): walk(child)
|
|
for suite in report.get('suites', []): walk(suite)
|
|
return result
|
|
|
|
def extract_performance(report, process_code):
|
|
tests = cases(report)
|
|
attachments = [a for test in tests for run in test.get('results', []) for a in run.get('attachments', [])
|
|
if a.get('name') == 'p9-requal-01-mariadb-performance']
|
|
if len(attachments) != 1: raise ValueError('Expected exactly one performance evidence attachment')
|
|
attachment = attachments[0]
|
|
if attachment.get('body'):
|
|
evidence = json.loads(base64.b64decode(attachment['body']))
|
|
elif attachment.get('path'):
|
|
path = Path(attachment['path'])
|
|
if not path.is_absolute(): path = UI / path
|
|
path = path.resolve()
|
|
if not path.is_relative_to(OUT.resolve()): raise ValueError('Attachment outside task artifacts')
|
|
evidence = json.loads(path.read_text(encoding='utf-8'))
|
|
else: raise ValueError('Performance attachment has no body/path')
|
|
passed = process_code == 0 and len(tests) == 1 and not report.get('errors') and tests[0].get('status') == 'expected'
|
|
passed = passed and len(tests[0].get('results', [])) == 1 and tests[0]['results'][0].get('status') == 'passed'
|
|
passed = passed and evidence.get('phase') == 'complete' and evidence.get('actualDurationMs', 0) >= 900000
|
|
return {**evidence, 'status': 'PASS' if passed else 'FAIL', 'processExitCode': process_code,
|
|
'attachmentName': 'p9-requal-01-mariadb-performance'}
|
|
|
|
def self_check():
|
|
schema = 'jt_it_' + 'a' * 32
|
|
assert schema_sql(schema, True) == f'DROP DATABASE `{schema}`;'
|
|
for bad in ('main', 'jt_it_' + 'a' * 31, schema + ';DROP DATABASE main'):
|
|
try: schema_sql(bad, True)
|
|
except ValueError: pass
|
|
else: raise AssertionError('Invalid schema accepted')
|
|
fixture = {'Name': '/' + CONTAINER, 'Id': 'test', 'Image': 'sha256:test', 'State': {'Running': True},
|
|
'Config': {'Image': IMAGE, 'Labels': {'com.docker.compose.project': 'jobtracker-it-p9-requal', 'com.docker.compose.service': 'mariadb-test'}},
|
|
'NetworkSettings': {'Ports': {'3306/tcp': [{'HostIp': '127.0.0.1', 'HostPort': '33308'}]}},
|
|
'HostConfig': {'Privileged': False, 'NetworkMode': 'bridge', 'Tmpfs': {'/var/lib/mysql': ''}}, 'Mounts': []}
|
|
assert ownership(fixture)['port'] == 33308
|
|
fixture['HostConfig']['NetworkMode'] = 'host'
|
|
try: ownership(fixture)
|
|
except ValueError: pass
|
|
else: raise AssertionError('Host networking accepted')
|
|
fixture['HostConfig']['NetworkMode'] = 'bridge'
|
|
fixture['Config']['Image'] = 'mariadb:latest'
|
|
try: ownership(fixture)
|
|
except ValueError: pass
|
|
else: raise AssertionError('Unpinned image accepted')
|
|
fixture['Config']['Image'] = IMAGE
|
|
fixture['NetworkSettings']['Ports']['3306/tcp'][0]['HostIp'] = '0.0.0.0'
|
|
try: ownership(fixture)
|
|
except ValueError: pass
|
|
else: raise AssertionError('Public database binding accepted')
|
|
evidence = {'phase': 'complete', 'actualDurationMs': 900000}
|
|
result = {'status': 'passed', 'attachments': [{'name': 'p9-requal-01-mariadb-performance', 'body': base64.b64encode(json.dumps(evidence).encode()).decode()}]}
|
|
report = {'suites': [{'specs': [{'tests': [{'status': 'expected', 'results': [result]}]}]}]}
|
|
assert extract_performance(report, 0)['status'] == 'PASS'
|
|
assert extract_performance(report, 1)['status'] == 'FAIL'
|
|
assert 'jt-integration-only' not in scrub('Server=127.0.0.1;Port=33308;Database=' + schema + ';User ID=root;Password=jt-integration-only;SslMode=None')
|
|
print('PASS: schema ownership/drop guards, pinned container/network guards, performance extraction/status and redaction; no services touched')
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument('mode', choices=list(CONFIGS) + ['self-check'])
|
|
args = parser.parse_args()
|
|
if args.mode == 'self-check': self_check(); return 0
|
|
lane = args.mode
|
|
config, expected = CONFIGS[lane]
|
|
report_path = OUT / (lane + '.json')
|
|
exit_path = OUT / (lane + '-exit.json')
|
|
results_path = OUT / (lane + '-results')
|
|
if any(path.exists() for path in (report_path, exit_path, results_path)) or (lane == 'performance' and (OUT / 'performance/result.json').exists()):
|
|
raise ValueError('Existing evidence present; archive it explicitly before a new invocation')
|
|
OUT.mkdir(parents=True, exist_ok=True)
|
|
env = {**os.environ, 'MYSQL_PWD': 'jt-integration-only', 'DOTNET_ROOT': 'C:/Users/Connor/.dotnet',
|
|
'DOTNET_HOST_PATH': 'C:/Users/Connor/.dotnet/dotnet.exe', 'PLAYWRIGHT_STATIC_EXPORT': 'true',
|
|
'PLAYWRIGHT_PREBUILT_EXPORT': 'true', 'PLAYWRIGHT_REUSE_SERVERS': 'false',
|
|
'PLAYWRIGHT_JSON_OUTPUT_NAME': str(report_path), 'JOBTRACKER_MARIADB_BROWSER_PORT': '33308',
|
|
'JOBTRACKER_MARIADB_CLIENT': str(CLIENT), 'NEXT_PUBLIC_API_BASE_URL': 'http://localhost:5302/api',
|
|
'SKIP_MODEL_LOAD': '1', 'AI_SERVICE_SKIP_MODEL_LOAD': '1', 'PYTHONIOENCODING': 'utf-8'}
|
|
schema = 'jt_it_' + uuid.uuid4().hex
|
|
created = False
|
|
process = None
|
|
record = {'lane': lane, 'startedUtc': utc(), 'generatedSchema': schema, 'commands': [], 'exitCode': 1, 'cleanup': {'attempted': False}}
|
|
started = time.monotonic()
|
|
def execute(command, name):
|
|
nonlocal created
|
|
completed = subprocess.run(['rtk', 'proxy', *command], cwd=ROOT, env=env, capture_output=True, text=True, encoding='utf-8', errors='replace', timeout=30)
|
|
# Cleanup ownership must survive a diagnostic write failing after successful CREATE.
|
|
if name == 'create-schema' and completed.returncode == 0: created = True
|
|
log = OUT / (lane + '-' + name + '.log')
|
|
log.write_text(scrub(completed.stdout + completed.stderr), encoding='utf-8')
|
|
record['commands'].append({'operation': name, 'exitCode': completed.returncode, 'log': str(log.relative_to(ROOT))})
|
|
if completed.returncode: raise RuntimeError(name + ' failed; inspect sanitized operation log')
|
|
return completed.stdout.strip()
|
|
def sql(query, name):
|
|
return execute([str(CLIENT), '--host=127.0.0.1', '--port=33308', '--user=root', '--batch', '--raw', '--skip-column-names', '--execute=' + query], name)
|
|
try:
|
|
for port in (5302, 3300):
|
|
for family, address in ((socket.AF_INET, '127.0.0.1'), (socket.AF_INET6, '::1')):
|
|
with socket.socket(family) as probe:
|
|
probe.settimeout(0.2)
|
|
if probe.connect_ex((address, port)) == 0: raise ValueError(f'Port {port} occupied; exclusive lane required')
|
|
# Inspect only this exact task container; full inspection stays in memory, never a report/log.
|
|
inspected = subprocess.run(['rtk', 'proxy', 'docker', 'inspect', CONTAINER], capture_output=True, text=True, timeout=20)
|
|
if inspected.returncode: raise ValueError('Task container inspection unavailable')
|
|
record['ownership'] = ownership(json.loads(inspected.stdout)[0])
|
|
version = sql('SELECT VERSION();', 'version')
|
|
record['serverVersion'] = version
|
|
if not re.fullmatch(r'12\.2\.2-MariaDB(?:-[A-Za-z0-9.]+)?', version): raise ValueError('Unexpected actual server version')
|
|
sql(schema_sql(schema), 'create-schema')
|
|
env['JOBTRACKER_MARIADB_BROWSER_CONNECTION'] = f'Server=127.0.0.1;Port=33308;Database={schema};User ID=root;Password=jt-integration-only;SslMode=None'
|
|
command = ['node', 'node_modules/@playwright/test/cli.js', 'test', '--config=' + config, '--retries=0', '--reporter=list,json', '--output=' + str(results_path)]
|
|
record['command'] = command
|
|
with (OUT / (lane + '.log')).open('w', encoding='utf-8') as log:
|
|
process = subprocess.Popen(['rtk', 'proxy', *command], cwd=UI, env=env, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, encoding='utf-8', errors='replace')
|
|
record['processId'] = process.pid
|
|
for line in process.stdout: log.write(scrub(line)); log.flush()
|
|
record['exitCode'] = process.wait()
|
|
report = json.loads(report_path.read_text(encoding='utf-8'))
|
|
write(report_path, report)
|
|
tests = cases(report)
|
|
record['caseCount'] = len(tests)
|
|
if len(tests) != expected or any(test.get('status') != 'expected' or len(test.get('results', [])) != 1 or test['results'][0].get('status') != 'passed' for test in tests) or report.get('errors'):
|
|
record['exitCode'] = record['exitCode'] or 1
|
|
if lane == 'performance': write(OUT / 'performance/result.json', extract_performance(report, record['exitCode']))
|
|
except Exception as error:
|
|
record['error'] = scrub(type(error).__name__ + ': ' + str(error))
|
|
record['exitCode'] = record['exitCode'] or 1
|
|
if lane == 'performance' and not (OUT / 'performance/result.json').exists():
|
|
write(OUT / 'performance/result.json', {'status': 'FAIL', 'reason': record['error'], 'processExitCode': record['exitCode']})
|
|
finally:
|
|
# Stop only this invocation's process tree before dropping its schema after an interrupted run.
|
|
if process is not None and process.poll() is None:
|
|
stopped = subprocess.run(['rtk', 'proxy', 'taskkill', '/PID', str(process.pid), '/T', '/F'], capture_output=True, text=True, timeout=20)
|
|
record['processTreeStopExitCode'] = stopped.returncode
|
|
try: process.wait(timeout=20)
|
|
except subprocess.TimeoutExpired:
|
|
created = False
|
|
record['cleanup']['error'] = 'Owned process tree still running; schema retained for operator cleanup'
|
|
record['exitCode'] = record['exitCode'] or 1
|
|
if created:
|
|
record['cleanup']['attempted'] = True
|
|
try:
|
|
sql(schema_sql(schema, True), 'drop-schema')
|
|
remaining = sql(f"SELECT COUNT(*) FROM information_schema.SCHEMATA WHERE SCHEMA_NAME='{schema}';", 'verify-drop')
|
|
if remaining != '0': raise ValueError('Generated schema remains after cleanup')
|
|
record['cleanup'].update(exitCode=0, schemaAbsent=True)
|
|
except Exception as error:
|
|
record['cleanup'].update(exitCode=1, error=scrub(str(error)))
|
|
record['exitCode'] = record['exitCode'] or 1
|
|
record.update(finishedUtc=utc(), durationSeconds=time.monotonic() - started,
|
|
helperSha256=hashlib.sha256(Path(__file__).read_bytes()).hexdigest())
|
|
if lane == 'performance' and (OUT / 'performance/result.json').exists():
|
|
result = json.loads((OUT / 'performance/result.json').read_text())
|
|
if record['exitCode']: result['status'] = 'FAIL'
|
|
result['cleanup'] = record['cleanup']
|
|
write(OUT / 'performance/result.json', result)
|
|
write(exit_path, record)
|
|
print(json.dumps({'lane': lane, 'exitCode': record['exitCode'], 'processRecord': str(exit_path)}))
|
|
return record['exitCode']
|
|
|
|
if __name__ == '__main__':
|
|
try: sys.exit(main())
|
|
except ValueError as error: raise SystemExit(scrub(str(error)))
|