142 lines
9.0 KiB
Python
142 lines
9.0 KiB
Python
"""Disposable local Linux renderer; no host daemon is exposed to the API container."""
|
|
import datetime, json, os, re, subprocess, sys, time, urllib.request, uuid
|
|
from pathlib import Path
|
|
|
|
ROOT = Path(__file__).resolve().parents[4]
|
|
OUT = ROOT / '.artifacts/p9-requal-01/linux'
|
|
IMAGE = 'jobtracker-p9-requal-api:security-check'
|
|
DB = 'jobtracker-it-p9-requal-mariadb-test-1'
|
|
PIN = 'mariadb:12.2.2@sha256:42a27baaba3e8f3f0159d16381d5a7156661aa91dcc65f663cabb6a6f9503c00'
|
|
VERSION = 'p9-requal-linux-2b26953'
|
|
|
|
def guard_schema(name):
|
|
if not re.fullmatch(r'jt_it_[0-9a-f]{32}', name):
|
|
raise ValueError('Only a generated qualification schema is allowed.')
|
|
return name
|
|
|
|
def command(args, **kwargs):
|
|
return subprocess.run(['rtk', 'proxy', *args], check=True, capture_output=True, text=True, encoding='utf-8', **kwargs).stdout.strip()
|
|
|
|
def sql(query):
|
|
return command(['docker', 'exec', '-e', 'MYSQL_PWD=jt-integration-only', DB,
|
|
'mariadb', '-uroot', '--batch', '--skip-column-names', '-e', query])
|
|
|
|
def safe_error(error):
|
|
# CalledProcessError includes command arguments, including development-only credentials.
|
|
return str(error).replace('jt-integration-only','[test-password]').replace('E2ePassword123!','[test-password]').replace('e2e-only-key-at-least-thirty-two-characters-long','[test-key]')
|
|
|
|
def main():
|
|
if '--self-test' in sys.argv:
|
|
assert guard_schema('jt_it_'+'a'*32)
|
|
for name in ['jobtracker', 'jt_it_abc', 'jt_it_'+'a'*32+';DROP DATABASE x']:
|
|
try: guard_schema(name)
|
|
except ValueError: continue
|
|
raise AssertionError(name)
|
|
print('Linux qualification schema guards: PASS')
|
|
return
|
|
OUT.mkdir(parents=True, exist_ok=True)
|
|
if (OUT/'process.json').exists() or (OUT.parent/'linux-renderer.json').exists():
|
|
raise ValueError('Retain prior evidence; select a fresh run directory.')
|
|
owned = json.loads(command(['docker','inspect',DB]))[0]
|
|
assert owned['Config']['Image'] == PIN
|
|
assert owned['Config']['Labels']['com.docker.compose.project'] == 'jobtracker-it-p9-requal'
|
|
assert owned['Config']['Labels']['com.docker.compose.service'] == 'mariadb-test'
|
|
assert '/var/lib/mysql' in owned['HostConfig'].get('Tmpfs',{})
|
|
assert not owned['HostConfig'].get('Privileged') and owned['HostConfig'].get('NetworkMode')!='host'
|
|
assert owned['State']['Running'] and owned['State']['Health']['Status']=='healthy'
|
|
assert owned['HostConfig']['PortBindings']['3306/tcp'] == [{'HostIp':'127.0.0.1','HostPort':'33308'}]
|
|
actual = sql('SELECT VERSION()')
|
|
assert actual.startswith('12.2.2-MariaDB'), actual
|
|
name=guard_schema('jt_it_'+uuid.uuid4().hex)
|
|
suffix=uuid.uuid4().hex[:12]
|
|
api='jobtracker-p9-requal-linux-'+suffix
|
|
network='jobtracker-p9-requal-linux-'+suffix
|
|
record={'startedUtc':datetime.datetime.now(datetime.timezone.utc).isoformat(), 'schema':name,
|
|
'serverVersion':actual,'imageTag':IMAGE,'apiContainer':api,'network':network,'cleanup':{},'exitCode':1}
|
|
created=attached=network_created=api_created=False
|
|
try:
|
|
assert sql(f"SELECT COUNT(*) FROM INFORMATION_SCHEMA.SCHEMATA WHERE SCHEMA_NAME='{name}'")=='0'
|
|
sql(f'CREATE DATABASE `{name}` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci')
|
|
created=True
|
|
assert sql(f"SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA='{name}'")=='0'
|
|
record['imageId']=command(['docker','image','inspect','--format','{{.Id}}',IMAGE])
|
|
record['chromium']=command(['docker','run','--rm','--network','none','--entrypoint','/usr/bin/chromium',IMAGE,'--version'])
|
|
record['runtime']=command(['docker','run','--rm','--network','none','--entrypoint','dotnet',IMAGE,'--info'])
|
|
command(['docker','network','create','--label','task=p9-requal-01',network]); network_created=True
|
|
command(['docker','network','connect','--alias','mariadb',network,DB]); attached=True
|
|
variables={
|
|
'ASPNETCORE_ENVIRONMENT':'Development','Database__Provider':'mariadb','Database__ServerVersion':'12.2.2',
|
|
'ConnectionStrings__JobTracker':f'Server=mariadb;Port=3306;Database={name};User ID=root;Password=jt-integration-only;SslMode=None',
|
|
'Auth__Require':'true','Auth__JwtKey':'e2e-only-key-at-least-thirty-two-characters-long',
|
|
'Auth__AdminEmail':'e2e@example.test','Auth__AdminPassword':'E2ePassword123!', 'Auth__AllowRegistration':'false',
|
|
'RateLimits__AuthLoginPermitLimit':'1000','Cors__Origins__0':'http://localhost:3300',
|
|
'Workers__AiOperationsEnabled':'false','Workers__SavedJobSearchAlertsEnabled':'false',
|
|
'HttpsRedirection__Enabled':'false','App__Version':VERSION,'App__CommitSha':'2b26953',
|
|
'Data__Root':'/data','Logging__LogLevel__Default':'Warning'}
|
|
args=['docker','create','--name',api,'--label','task=p9-requal-01','--network',network,
|
|
'-p','127.0.0.1:5302:8080','--tmpfs','/data:rw,nosuid,size=512m']
|
|
for key,value in variables.items(): args+=['-e',key+'='+value]
|
|
command(args+[IMAGE]); api_created=True
|
|
assert command(['docker','inspect','--format','{{.Image}}',api])==record['imageId']
|
|
command(['docker','start',api])
|
|
deadline=time.monotonic()+120
|
|
while True:
|
|
try:
|
|
with urllib.request.urlopen('http://localhost:5302/health',timeout=3) as response:
|
|
health=json.load(response)
|
|
assert health['version']==VERSION, health
|
|
with urllib.request.urlopen('http://localhost:5302/ready',timeout=3) as response:
|
|
assert json.load(response)['status']=='ready'
|
|
break
|
|
except Exception:
|
|
if time.monotonic()>=deadline: raise
|
|
time.sleep(1)
|
|
record['health']=health
|
|
record['migrationCount']=int(sql(f'SELECT COUNT(*) FROM `{name}`.`__EFMigrationsHistory`'))
|
|
assert record['migrationCount']>0
|
|
env={**os.environ,'P9_REQUAL_LINUX_READY':VERSION,'PLAYWRIGHT_STATIC_EXPORT':'true',
|
|
'PLAYWRIGHT_PREBUILT_EXPORT':'true','NEXT_PUBLIC_API_BASE_URL':'http://localhost:5302/api',
|
|
'CV_FIDELITY_OUTPUT':str(OUT/'fidelity'),'CV_SETTINGS_FIDELITY_OUTPUT':str(OUT/'settings-fidelity'),
|
|
'PLAYWRIGHT_JSON_OUTPUT_NAME':str(OUT.parent/'linux-renderer.json')}
|
|
test=['node','node_modules/@playwright/test/cli.js','test',
|
|
'--config=../docs/redesign/phase-9/p9-requal-01/linux/playwright.config.ts',
|
|
'--retries=0','--reporter=list,json','--output='+str(OUT/'test-results')]
|
|
with (OUT/'playwright.log').open('w',encoding='utf-8') as log:
|
|
record['browserExitCode']=subprocess.run(['rtk','proxy',*test],cwd=ROOT/'job-tracker-ui',env=env,stdout=log,stderr=subprocess.STDOUT).returncode
|
|
record['exitCode']=record['browserExitCode']
|
|
record['migrationCountAfter']=int(sql(f'SELECT COUNT(*) FROM `{name}`.`__EFMigrationsHistory`'))
|
|
assert record['migrationCountAfter']==record['migrationCount']
|
|
except Exception as error:
|
|
record['exitCode']=1
|
|
record['error']=safe_error(error)
|
|
finally:
|
|
if api_created:
|
|
try:
|
|
(OUT/'api.log').write_text(safe_error(command(['docker','logs',api])),encoding='utf-8')
|
|
except Exception as error: record['cleanup']['logError']=safe_error(error)
|
|
try:
|
|
command(['docker','rm','--force',api]); record['cleanup']['apiRemoved']=True
|
|
except Exception as error: record['cleanup']['apiError']=str(error)
|
|
if attached:
|
|
try: command(['docker','network','disconnect',network,DB]); record['cleanup']['databaseDisconnected']=True
|
|
except Exception as error: record['cleanup']['disconnectError']=str(error)
|
|
if network_created:
|
|
try: command(['docker','network','rm',network]); record['cleanup']['networkRemoved']=True
|
|
except Exception as error: record['cleanup']['networkError']=str(error)
|
|
if created and (not api_created or record['cleanup'].get('apiRemoved')):
|
|
try:
|
|
guard_schema(name); sql(f'DROP DATABASE `{name}`')
|
|
assert sql(f"SELECT COUNT(*) FROM INFORMATION_SCHEMA.SCHEMATA WHERE SCHEMA_NAME='{name}'")=='0'
|
|
record['cleanup']['schemaRemoved']=True
|
|
except Exception as error: record['cleanup']['schemaError']=str(error)
|
|
elif created:
|
|
record['cleanup']['schemaError']='Schema retained because API removal was not confirmed.'
|
|
if any(key.endswith('Error') for key in record['cleanup']): record['exitCode']=1
|
|
record['finishedUtc']=datetime.datetime.now(datetime.timezone.utc).isoformat()
|
|
(OUT/'process.json').write_text(json.dumps(record,indent=2)+'\n',encoding='utf-8')
|
|
(OUT.parent/'linux-renderer-exit.json').write_text(json.dumps(record,indent=2)+'\n',encoding='utf-8')
|
|
print(json.dumps(record))
|
|
if record['exitCode'] or any(key.endswith('Error') for key in record['cleanup']): raise SystemExit(1)
|
|
|
|
if __name__=='__main__': main()
|