2073 lines
84 KiB
Python
2073 lines
84 KiB
Python
#!/usr/bin/env python3
|
|
"""Exercise the real packaged Windows and Linux remote workers end to end.
|
|
|
|
Docker is intentionally reached only through the configured WSL distribution.
|
|
The verifier creates individually named and labelled resources, never invokes
|
|
Compose or a broad cleanup command, and retains all owned artifacts on failure.
|
|
"""
|
|
|
|
import argparse
|
|
import hashlib
|
|
import json
|
|
import os
|
|
from pathlib import Path
|
|
import re
|
|
import secrets
|
|
import shutil
|
|
import socket
|
|
import ssl
|
|
import stat
|
|
import subprocess
|
|
import sys
|
|
import threading
|
|
import time
|
|
|
|
|
|
LINUX_IMAGE = 'truf-remote-worker:linux-x86_64'
|
|
TEST_IMAGE = 'truf-worker-test:test'
|
|
WINDOWS_ARTIFACT = 'dist/truf-worker-windows-x86_64'
|
|
TARGETS = (
|
|
'https://gitlab.com/truf-e2e/a.git',
|
|
'https://gitlab.com/truf-e2e/b.git',
|
|
)
|
|
DOCKER_TARGET = 'docker.io/truf-e2e/synthetic@sha256:' + ('7' * 64)
|
|
HUGGINGFACE_TARGET = 'truf-e2e/synthetic'
|
|
DIRECT_TARGETS = {
|
|
'dockerhub': DOCKER_TARGET,
|
|
'huggingface': HUGGINGFACE_TARGET,
|
|
}
|
|
ALL_TARGETS = (*TARGETS, DOCKER_TARGET, HUGGINGFACE_TARGET)
|
|
PARALLELISM = 2
|
|
MAX_OUTPUT = 2 * 1024 * 1024
|
|
MAX_DOCKER_RESOURCES = 1024
|
|
MAX_SNAPSHOT_BYTES = 8 * 1024 * 1024
|
|
MAX_SAFE_EVIDENCE = 64 * 1024
|
|
RUN_LABEL = 'com.truf.packaged-worker-e2e.run'
|
|
PHASE_LABEL = 'com.truf.packaged-worker-e2e.phase'
|
|
KIND_LABEL = 'com.truf.packaged-worker-e2e.kind'
|
|
RESOURCE_NAME = re.compile(r'truf-packaged-worker-e2e-[a-f0-9]{16}-(windows|linux)-[a-z-]+')
|
|
HEX_40 = re.compile(r'[a-f0-9]{40}')
|
|
HEX_64 = re.compile(r'[a-f0-9]{64}')
|
|
|
|
CONTAINER_METADATA_FORMAT = (
|
|
'{"id":{{json .Id}},"name":{{json .Name}},"image":{{json .Image}},'
|
|
'"status":{{json .State.Status}},"running":{{json .State.Running}},'
|
|
'"paused":{{json .State.Paused}},"restarting":{{json .State.Restarting}},'
|
|
'"dead":{{json .State.Dead}},"mounts":{{json .Mounts}}}'
|
|
)
|
|
VOLUME_METADATA_FORMAT = (
|
|
'{"name":{{json .Name}},"driver":{{json .Driver}},"scope":{{json .Scope}},'
|
|
'"created":{{json .CreatedAt}},"mountpoint":{{json .Mountpoint}},'
|
|
'"labels":{{json .Labels}},"options":{{json .Options}}}'
|
|
)
|
|
|
|
|
|
def isolated_test_subnet(run_id, scope):
|
|
value = int.from_bytes(
|
|
hashlib.sha256(f'{run_id}\0{scope}'.encode('ascii')).digest()[:2], 'big',
|
|
) & 0x1fff
|
|
return f'198.{18 + (value >> 12)}.{(value >> 4) & 0xff}.{(value & 0xf) * 16}/28'
|
|
|
|
|
|
LOOPBACK_PROXY_SCRIPT = r'''import socket
|
|
import sys
|
|
import threading
|
|
|
|
listen_port = int(sys.argv[1])
|
|
upstream_host = sys.argv[2]
|
|
upstream_port = int(sys.argv[3])
|
|
|
|
|
|
def pump(source, destination):
|
|
try:
|
|
while True:
|
|
block = source.recv(64 * 1024)
|
|
if not block:
|
|
break
|
|
destination.sendall(block)
|
|
except OSError:
|
|
pass
|
|
try:
|
|
destination.shutdown(socket.SHUT_WR)
|
|
except OSError:
|
|
pass
|
|
|
|
|
|
def bridge(client):
|
|
upstream = None
|
|
try:
|
|
upstream = socket.create_connection((upstream_host, upstream_port), timeout=5)
|
|
print('proxy upstream connected', flush=True)
|
|
client.settimeout(None)
|
|
upstream.settimeout(None)
|
|
outbound = threading.Thread(target=pump, args=(client, upstream), daemon=True)
|
|
outbound.start()
|
|
pump(upstream, client)
|
|
outbound.join(5)
|
|
except OSError as exc:
|
|
print('proxy upstream failed: ' + type(exc).__name__, flush=True)
|
|
finally:
|
|
client.close()
|
|
if upstream is not None:
|
|
upstream.close()
|
|
|
|
|
|
listener = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
listener.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
listener.bind(('127.0.0.1', listen_port))
|
|
listener.listen(32)
|
|
print('proxy ready', flush=True)
|
|
while True:
|
|
connection, _ = listener.accept()
|
|
threading.Thread(target=bridge, args=(connection,), daemon=True).start()
|
|
'''
|
|
|
|
|
|
SEED_SCRIPT = r'''import os
|
|
import stat
|
|
|
|
root = '/seed'
|
|
for current, directories, files in os.walk(root, topdown=False, followlinks=False):
|
|
for name in directories + files:
|
|
path = os.path.join(current, name)
|
|
details = os.lstat(path)
|
|
if stat.S_ISLNK(details.st_mode) or not (
|
|
stat.S_ISDIR(details.st_mode) or stat.S_ISREG(details.st_mode)
|
|
):
|
|
raise SystemExit('unsupported seed entry')
|
|
os.chown(path, 10001, 10001, follow_symlinks=False)
|
|
os.chmod(path, 0o700 if stat.S_ISDIR(details.st_mode) else 0o600)
|
|
os.chown(root, 10001, 10001, follow_symlinks=False)
|
|
os.chmod(root, 0o700)
|
|
os.unlink('/seed/_seed.py')
|
|
'''
|
|
|
|
|
|
INSPECT_WORKER_SCRIPT = r'''import hashlib
|
|
import json
|
|
import os
|
|
from pathlib import Path
|
|
import stat
|
|
|
|
|
|
def digest(path):
|
|
value = hashlib.sha256()
|
|
with open(path, 'rb', buffering=0) as handle:
|
|
for block in iter(lambda: handle.read(1024 * 1024), b''):
|
|
value.update(block)
|
|
return value.hexdigest()
|
|
|
|
|
|
def regular(path):
|
|
details = os.stat(path, follow_symlinks=False)
|
|
if not stat.S_ISREG(details.st_mode) or os.path.islink(path):
|
|
raise RuntimeError('non-regular worker artifact')
|
|
return details
|
|
|
|
|
|
def tree_identity(root):
|
|
identity = hashlib.sha256()
|
|
count = 0
|
|
active_entries = 0
|
|
root = Path(root)
|
|
if not root.exists():
|
|
return {'count': 0, 'active_entries': 0, 'sha256': identity.hexdigest()}
|
|
for current, directories, files in os.walk(root, followlinks=False):
|
|
directories.sort()
|
|
files.sort()
|
|
for name in directories:
|
|
path = Path(current) / name
|
|
if path.is_symlink() or not path.is_dir():
|
|
raise RuntimeError('unsupported worker directory')
|
|
if path.relative_to(root).parts[0] != 'abandoned':
|
|
active_entries += 1
|
|
for name in files:
|
|
path = Path(current) / name
|
|
details = regular(path)
|
|
if path.relative_to(root).parts[0] != 'abandoned':
|
|
active_entries += 1
|
|
relative = path.relative_to(root).as_posix().encode('utf-8')
|
|
identity.update(relative + b'\0')
|
|
identity.update(str(details.st_size).encode('ascii') + b'\0')
|
|
identity.update(str(details.st_mtime_ns).encode('ascii') + b'\0')
|
|
identity.update(digest(path).encode('ascii') + b'\n')
|
|
count += 1
|
|
return {
|
|
'count': count, 'active_entries': active_entries,
|
|
'sha256': identity.hexdigest(),
|
|
}
|
|
|
|
|
|
states = {}
|
|
state_root = Path('/data/state-base/truf/remote-worker')
|
|
if state_root.exists():
|
|
for path in sorted(state_root.glob('slot-*.json')):
|
|
details = regular(path)
|
|
with open(path, 'rb') as handle:
|
|
value = json.load(handle)
|
|
reservation = dict((value.get('assignment') or {}).get('reservation') or {})
|
|
assignment = dict(value.get('assignment') or {})
|
|
snapshot = dict(assignment.get('execution_snapshot') or {})
|
|
planning = dict(snapshot.get('planning') or {})
|
|
states[path.name] = {
|
|
'phase': value.get('phase'),
|
|
'reservation_id': reservation.get('reservation_id'),
|
|
'bundle_id': reservation.get('bundle_id'),
|
|
'scan_event_id': reservation.get('scan_event_id'),
|
|
'source': reservation.get('source'),
|
|
'platform': reservation.get('platform'),
|
|
'target': reservation.get('target'),
|
|
'planning_kind': planning.get('kind'),
|
|
'sha256': digest(path),
|
|
'size': details.st_size,
|
|
'mtime_ns': details.st_mtime_ns,
|
|
}
|
|
|
|
bundles = {}
|
|
bundle_root = Path('/data/client/truf/remote-worker/bundles')
|
|
if bundle_root.exists():
|
|
for path in sorted(bundle_root.rglob('*.trb')):
|
|
details = regular(path)
|
|
bundles[path.relative_to(bundle_root).as_posix()] = {
|
|
'sha256': digest(path),
|
|
'size': details.st_size,
|
|
'mtime_ns': details.st_mtime_ns,
|
|
}
|
|
|
|
shutdown_receipt = None
|
|
receipt_path = state_root / 'control' / 'worker.exit.json'
|
|
if receipt_path.exists():
|
|
regular(receipt_path)
|
|
with open(receipt_path, 'rb') as handle:
|
|
shutdown_receipt = json.load(handle)
|
|
|
|
print(json.dumps({
|
|
'states': states,
|
|
'bundles': bundles,
|
|
'work': tree_identity('/data/client/truf/remote-worker/work'),
|
|
'shutdown_receipt': shutdown_receipt,
|
|
}, ensure_ascii=True, sort_keys=True, separators=(',', ':')))
|
|
'''
|
|
|
|
PUBLISH_DIRECT_SCRIPT = r'''import json
|
|
import os
|
|
from datetime import datetime, timezone
|
|
from pathlib import Path
|
|
import stat
|
|
import sys
|
|
|
|
|
|
def fail(message):
|
|
raise RuntimeError('direct bundle publisher: ' + message)
|
|
|
|
|
|
def require(condition, message):
|
|
if not condition:
|
|
fail(message)
|
|
|
|
|
|
require(len(sys.argv) == 4, 'arguments')
|
|
package_root = Path(sys.argv[1]).resolve(strict=True)
|
|
state_root = Path(sys.argv[2]).resolve(strict=True)
|
|
bundle_root = Path(sys.argv[3]).resolve(strict=True)
|
|
manifest_path = package_root / 'worker-package.json'
|
|
with open(manifest_path, 'rb') as handle:
|
|
bootstrap_manifest = json.load(handle)
|
|
app_root = package_root / str(bootstrap_manifest.get('app_root') or '')
|
|
require(app_root.is_dir(), 'application root')
|
|
sys.path.insert(0, os.fspath(app_root / 'dependencies'))
|
|
sys.path.insert(0, os.fspath(app_root))
|
|
|
|
from scan_execution import ( # noqa: E402
|
|
QueueDispositionPolicy,
|
|
stage_scan_result_in_scope,
|
|
validate_protocol2_remote_assignment,
|
|
)
|
|
from worker_package import ( # noqa: E402
|
|
load_worker_package_manifest,
|
|
worker_package_build_compatibility,
|
|
)
|
|
|
|
manifest = load_worker_package_manifest(manifest_path)
|
|
local_build = worker_package_build_compatibility(manifest)
|
|
capabilities = tuple(
|
|
(item['source'], item['platform'], item['planning_kind'])
|
|
for item in manifest['capabilities']
|
|
)
|
|
expected = {
|
|
('dockerhub', 'docker', 'docker_direct_v1'),
|
|
('huggingface', 'huggingface', 'huggingface_space_v1'),
|
|
}
|
|
state_paths = sorted(state_root.glob('slot-*.json'))
|
|
require({path.name for path in state_paths} == {'slot-0.json', 'slot-1.json'}, 'slot states')
|
|
require(not any(bundle_root.rglob('*.trb')), 'bundle storage is not empty')
|
|
seen = set()
|
|
for path in state_paths:
|
|
details = path.stat(follow_symlinks=False)
|
|
require(stat.S_ISREG(details.st_mode) and not path.is_symlink(), 'regular slot state')
|
|
require(0 < details.st_size <= 64 * 1024 * 1024, 'slot state bound')
|
|
with open(path, 'rb') as handle:
|
|
state = json.load(handle)
|
|
require(state.get('phase') == 'assigned', 'slot phase')
|
|
assignment = dict(state.get('assignment') or {})
|
|
validated = validate_protocol2_remote_assignment(
|
|
assignment, local_build, capabilities,
|
|
)
|
|
reservation = validated['reservation']
|
|
identity = (
|
|
reservation.source, reservation.platform, validated['planning_kind'],
|
|
)
|
|
require(identity in expected and identity not in seen, 'direct capability')
|
|
require(
|
|
validated['snapshot']['credential_ref'] == {
|
|
'source': reservation.source, 'auth_entry': '',
|
|
},
|
|
'direct credential reference',
|
|
)
|
|
now = datetime.now(timezone.utc).isoformat()
|
|
result = {
|
|
'target': reservation.target,
|
|
'scan_type': reservation.platform,
|
|
'scan_event_id': reservation.scan_event_id,
|
|
'findings': [],
|
|
'errors': [],
|
|
'warnings': [],
|
|
'scan_started_at': now,
|
|
'timestamp': now,
|
|
'duration_sec': 0.0,
|
|
'scan_meta': {'synthetic_claim_to_ingestion': True},
|
|
}
|
|
limits = dict(assignment['limits'])
|
|
staged = stage_scan_result_in_scope(
|
|
result,
|
|
reservation,
|
|
os.fspath(bundle_root),
|
|
assignment['event_scan_options'],
|
|
QueueDispositionPolicy(**dict(assignment['queue_policy'])),
|
|
attempts=int(assignment['reservation'].get('attempts') or 1),
|
|
candidate_max_items=int(limits.get('candidate_max_items') or 2000),
|
|
candidate_max_bytes=int(limits.get('candidate_max_bytes') or 2 * 1024 * 1024),
|
|
require_s_drive=False,
|
|
)
|
|
require(
|
|
staged.queue_status == 'done'
|
|
and staged.finding_count == staged.error_count == staged.candidate_count == 0,
|
|
'clean staged bundle',
|
|
)
|
|
seen.add(identity)
|
|
require(seen == expected, 'direct source coverage')
|
|
require(len(tuple(bundle_root.rglob('*.trb'))) == 2, 'published bundle count')
|
|
print('published direct bundles: 2')
|
|
'''
|
|
|
|
|
|
class Failure(RuntimeError):
|
|
"""A fixed diagnostic label that cannot contain credentials or findings."""
|
|
|
|
|
|
def require(condition, label):
|
|
if not condition:
|
|
raise Failure(label)
|
|
|
|
|
|
def sha256_bytes(value):
|
|
return hashlib.sha256(value).hexdigest()
|
|
|
|
|
|
def sha256_file(path):
|
|
digest = hashlib.sha256()
|
|
with open(path, 'rb', buffering=0) as handle:
|
|
for block in iter(lambda: handle.read(1024 * 1024), b''):
|
|
digest.update(block)
|
|
return digest.hexdigest()
|
|
|
|
|
|
def write_bytes(path, value):
|
|
path = Path(path)
|
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
with open(path, 'xb') as handle:
|
|
handle.write(value)
|
|
|
|
|
|
def write_json(path, value):
|
|
write_bytes(
|
|
path,
|
|
json.dumps(
|
|
value, ensure_ascii=True, sort_keys=True, separators=(',', ':'),
|
|
).encode('ascii') + b'\n',
|
|
)
|
|
|
|
|
|
def load_json_bytes(value, label):
|
|
require(len(value) <= MAX_OUTPUT, label + '_size')
|
|
try:
|
|
result = json.loads(value.decode('utf-8', errors='strict'))
|
|
except (UnicodeError, ValueError):
|
|
raise Failure(label + '_json') from None
|
|
return result
|
|
|
|
|
|
def regular_file(path, label, maximum=MAX_OUTPUT):
|
|
path = Path(path)
|
|
try:
|
|
details = path.stat(follow_symlinks=False)
|
|
except OSError:
|
|
raise Failure(label + '_missing') from None
|
|
require(
|
|
stat.S_ISREG(details.st_mode) and not path.is_symlink()
|
|
and 0 < details.st_size <= maximum,
|
|
label + '_regular',
|
|
)
|
|
return path
|
|
|
|
|
|
def synthetic_openai_token(label):
|
|
"""Return a fixed, deliberately fake, detector-shaped high-entropy token."""
|
|
alphabet = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
|
|
|
|
def material(part):
|
|
seed = hashlib.sha512(
|
|
('truf-packaged-worker-e2e:' + label + ':' + part).encode('ascii')
|
|
).hexdigest()
|
|
return ''.join(
|
|
alphabet[int(seed[index:index + 2], 16) % len(alphabet)]
|
|
for index in range(0, len(seed), 2)
|
|
)[:24]
|
|
|
|
token = 'sk-proj-' + material('prefix') + 'T3BlbkFJ' + material('suffix')
|
|
require(
|
|
re.fullmatch(r'sk-proj-[A-Za-z0-9]{24}T3BlbkFJ[A-Za-z0-9]{24}', token),
|
|
'synthetic_token_shape',
|
|
)
|
|
return token
|
|
|
|
|
|
class CommandRunner:
|
|
def __init__(self, root, run_root, distro, deadline):
|
|
self.root = Path(root)
|
|
self.run_root = Path(run_root)
|
|
self.deadline = deadline
|
|
self.wsl = shutil.which('wsl.exe') or shutil.which('wsl')
|
|
require(self.wsl, 'wsl_unavailable')
|
|
self.docker_prefix = [
|
|
self.wsl, '-d', distro, '--', 'sudo', '-n', 'docker',
|
|
]
|
|
allowed = (
|
|
'PATH', 'PATHEXT', 'SystemRoot', 'SYSTEMROOT', 'WINDIR', 'COMSPEC',
|
|
'TEMP', 'TMP', 'USERPROFILE', 'WSLENV',
|
|
)
|
|
self.host_env = {
|
|
name: os.environ[name] for name in allowed if name in os.environ
|
|
}
|
|
|
|
def execute(self, label, command, *, timeout=60, check=True, env=None, cwd=None):
|
|
remaining = self.deadline - time.monotonic()
|
|
require(remaining > 0, 'aggregate_timeout')
|
|
timeout = max(0.1, min(float(timeout), remaining))
|
|
process = None
|
|
try:
|
|
process = subprocess.Popen(
|
|
command,
|
|
cwd=os.fspath(cwd or self.root),
|
|
env=env or self.host_env,
|
|
stdin=subprocess.DEVNULL,
|
|
stdout=subprocess.PIPE,
|
|
stderr=subprocess.PIPE,
|
|
creationflags=getattr(subprocess, 'CREATE_NO_WINDOW', 0),
|
|
)
|
|
try:
|
|
stdout, stderr = process.communicate(timeout=timeout)
|
|
except subprocess.TimeoutExpired:
|
|
process.kill()
|
|
process.communicate()
|
|
raise Failure(label + '_timeout') from None
|
|
except Failure:
|
|
raise
|
|
except OSError:
|
|
raise Failure(label + '_unavailable') from None
|
|
require(
|
|
len(stdout) <= MAX_OUTPUT and len(stderr) <= MAX_OUTPUT,
|
|
label + '_output_bound',
|
|
)
|
|
if check:
|
|
if process.returncode != 0:
|
|
error = Failure(label + '_failed')
|
|
error.exit_code = process.returncode
|
|
raise error
|
|
return process.returncode, stdout, stderr
|
|
|
|
def docker(self, label, arguments, *, timeout=60, check=True):
|
|
return self.execute(
|
|
label, [*self.docker_prefix, *arguments], timeout=timeout, check=check,
|
|
)
|
|
|
|
def docker_json(self, label, arguments, *, timeout=60):
|
|
_, stdout, _ = self.docker(label, arguments, timeout=timeout)
|
|
return load_json_bytes(stdout, label)
|
|
|
|
def wsl_path(self, path):
|
|
_, stdout, _ = self.execute(
|
|
'wsl_path',
|
|
[self.wsl, '-d', self.docker_prefix[2], '--', 'wslpath', '-a', '-u',
|
|
os.fspath(path).replace('\\', '/')],
|
|
timeout=30,
|
|
)
|
|
try:
|
|
value = stdout.decode('utf-8', errors='strict').strip()
|
|
except UnicodeError:
|
|
raise Failure('wsl_path_encoding') from None
|
|
require(value.startswith('/') and '\x00' not in value, 'wsl_path_invalid')
|
|
return value
|
|
|
|
|
|
class ResourceTracker:
|
|
def __init__(self, runner, run_id):
|
|
self.runner = runner
|
|
self.run_id = run_id
|
|
self.resources = {'containers': {}, 'volumes': {}, 'networks': {}}
|
|
self.foreign_baseline = None
|
|
|
|
def labels(self, phase, kind):
|
|
return [
|
|
'--label', f'{RUN_LABEL}={self.run_id}',
|
|
'--label', f'{PHASE_LABEL}={phase}',
|
|
'--label', f'{KIND_LABEL}={kind}',
|
|
]
|
|
|
|
@staticmethod
|
|
def singular(kind):
|
|
return {'containers': 'container', 'volumes': 'volume', 'networks': 'network'}[kind]
|
|
|
|
def list_names(self, kind, *, name=None, owned=False):
|
|
singular = self.singular(kind)
|
|
arguments = [singular, 'ls']
|
|
if kind == 'containers':
|
|
arguments.append('--all')
|
|
if name is not None:
|
|
pattern = '^/' + name + '$' if kind == 'containers' else '^' + name + '$'
|
|
arguments.extend(['--filter', 'name=' + pattern])
|
|
if owned:
|
|
arguments.extend(['--filter', f'label={RUN_LABEL}={self.run_id}'])
|
|
field = 'Names' if kind == 'containers' else 'Name'
|
|
arguments.append('--format={{.' + field + '}}')
|
|
_, stdout, _ = self.runner.docker('list_' + kind, arguments)
|
|
try:
|
|
names = {line for line in stdout.decode('ascii').splitlines() if line}
|
|
except UnicodeError:
|
|
raise Failure('resource_name_encoding') from None
|
|
require(len(names) <= 32, 'resource_inventory_bound')
|
|
return names
|
|
|
|
def guard_new(self, kind, name):
|
|
require(RESOURCE_NAME.fullmatch(name), 'resource_name_guard')
|
|
require(not self.list_names(kind, name=name), 'resource_name_collision')
|
|
|
|
def inspect_owned(self, kind, name, expected=None):
|
|
singular = self.singular(kind)
|
|
if kind == 'containers':
|
|
template = (
|
|
'{"id":{{json .Id}},"name":{{json .Name}},'
|
|
'"run":{{json (index .Config.Labels "' + RUN_LABEL + '")}},'
|
|
'"phase":{{json (index .Config.Labels "' + PHASE_LABEL + '")}},'
|
|
'"kind":{{json (index .Config.Labels "' + KIND_LABEL + '")}},'
|
|
'"running":{{json .State.Running}},"paused":{{json .State.Paused}},'
|
|
'"restarting":{{json .State.Restarting}}}'
|
|
)
|
|
else:
|
|
template = (
|
|
'{"id":{{json ' + ('.Id' if kind == 'networks' else '""') + '}},'
|
|
'"name":{{json .Name}},"run":{{json (index .Labels "' + RUN_LABEL + '")}},'
|
|
'"phase":{{json (index .Labels "' + PHASE_LABEL + '")}},'
|
|
'"kind":{{json (index .Labels "' + KIND_LABEL + '")}}}'
|
|
)
|
|
value = self.runner.docker_json(
|
|
'inspect_owned_' + singular, [singular, 'inspect', '--format', template, name],
|
|
)
|
|
require(
|
|
value.get('name') == ('/' + name if kind == 'containers' else name)
|
|
and value.get('run') == self.run_id
|
|
and isinstance(value.get('phase'), str) and isinstance(value.get('kind'), str)
|
|
and (kind == 'volumes' or HEX_64.fullmatch(str(value.get('id') or ''))),
|
|
'owned_' + singular + '_identity_guard',
|
|
)
|
|
if expected is not None:
|
|
require(
|
|
(expected.get('id') is None or value.get('id') == expected['id'])
|
|
and value['phase'] == expected['phase'] and value['kind'] == expected['kind'],
|
|
'owned_' + singular + '_identity_changed',
|
|
)
|
|
return value
|
|
|
|
def metadata_names(self, kind):
|
|
arguments = ([kind, 'ls', '--all', '--no-trunc', '--format={{.ID}}']
|
|
if kind == 'container' else [kind, 'ls', '--format={{.Name}}'])
|
|
_, stdout, _ = self.runner.docker('foreign_' + kind + '_list', arguments)
|
|
try:
|
|
values = {line for line in stdout.decode('ascii').splitlines() if line}
|
|
except UnicodeError:
|
|
raise Failure('foreign_' + kind + '_inventory_encoding') from None
|
|
require(len(values) <= MAX_DOCKER_RESOURCES, 'foreign_' + kind + '_inventory_bound')
|
|
return values
|
|
|
|
def container_metadata(self, identifier):
|
|
require(HEX_64.fullmatch(identifier), 'foreign_container_id_guard')
|
|
value = self.runner.docker_json(
|
|
'foreign_container_inspect',
|
|
['container', 'inspect', '--format', CONTAINER_METADATA_FORMAT, identifier],
|
|
)
|
|
require(
|
|
value.get('id') == identifier and isinstance(value.get('name'), str)
|
|
and HEX_64.fullmatch(str(value.get('image') or '').removeprefix('sha256:'))
|
|
and value.get('status') in (
|
|
'created', 'running', 'paused', 'restarting', 'removing', 'exited', 'dead',
|
|
)
|
|
and all(type(value.get(key)) is bool for key in (
|
|
'running', 'paused', 'restarting', 'dead',
|
|
))
|
|
and isinstance(value.get('mounts'), list) and len(value['mounts']) <= 128,
|
|
'foreign_container_metadata_guard',
|
|
)
|
|
mounts = [{
|
|
key: mount.get(key) for key in (
|
|
'Type', 'Name', 'Source', 'Destination', 'Driver', 'Mode', 'RW', 'Propagation',
|
|
)
|
|
} for mount in value['mounts']]
|
|
return {
|
|
'id': value['id'], 'name': value['name'], 'image': value['image'],
|
|
'status': value['status'], 'running': value['running'], 'paused': value['paused'],
|
|
'restarting': value['restarting'], 'dead': value['dead'],
|
|
'mounts_sha256': sha256_bytes(json.dumps(
|
|
mounts, ensure_ascii=True, sort_keys=True, separators=(',', ':'),
|
|
).encode('ascii')),
|
|
}
|
|
|
|
def volume_metadata(self, name):
|
|
value = self.runner.docker_json(
|
|
'foreign_volume_inspect',
|
|
['volume', 'inspect', '--format', VOLUME_METADATA_FORMAT, name],
|
|
)
|
|
require(
|
|
value.get('name') == name and isinstance(value.get('driver'), str)
|
|
and isinstance(value.get('scope'), str) and isinstance(value.get('mountpoint'), str)
|
|
and (value.get('created') is None or isinstance(value['created'], str))
|
|
and (value.get('labels') is None or isinstance(value['labels'], dict))
|
|
and (value.get('options') is None or isinstance(value['options'], dict)),
|
|
'foreign_volume_metadata_guard',
|
|
)
|
|
return {
|
|
'name': name, 'driver': value['driver'], 'scope': value['scope'],
|
|
'created': value['created'],
|
|
'mountpoint_sha256': sha256_bytes(value['mountpoint'].encode('utf-8')),
|
|
'labels_sha256': sha256_bytes(json.dumps(
|
|
value['labels'], ensure_ascii=True, sort_keys=True, separators=(',', ':'),
|
|
).encode('ascii')),
|
|
'options_sha256': sha256_bytes(json.dumps(
|
|
value['options'], ensure_ascii=True, sort_keys=True, separators=(',', ':'),
|
|
).encode('ascii')),
|
|
}
|
|
|
|
def metadata_snapshot(self, *, exclude_owned=False):
|
|
containers = {}
|
|
owned_ids = {
|
|
value['id']: (name, value)
|
|
for name, value in self.resources['containers'].items() if value.get('id')
|
|
}
|
|
for identifier in sorted(self.metadata_names('container')):
|
|
if exclude_owned and identifier in owned_ids:
|
|
name, expected = owned_ids[identifier]
|
|
self.inspect_owned('containers', name, expected)
|
|
continue
|
|
containers[identifier] = self.container_metadata(identifier)
|
|
volumes = {}
|
|
for name in sorted(self.metadata_names('volume')):
|
|
value = self.volume_metadata(name)
|
|
expected = self.resources['volumes'].get(name)
|
|
if exclude_owned and expected is not None and expected.get('metadata') == value:
|
|
continue
|
|
volumes[name] = value
|
|
snapshot = {'containers': containers, 'volumes': volumes}
|
|
require(len(json.dumps(snapshot, ensure_ascii=True, sort_keys=True)) <= MAX_SNAPSHOT_BYTES,
|
|
'foreign_metadata_snapshot_bound')
|
|
return snapshot
|
|
|
|
def snapshot_foreign(self):
|
|
require(self.foreign_baseline is None, 'foreign_snapshot_already_taken')
|
|
self.foreign_baseline = self.metadata_snapshot()
|
|
|
|
def assert_foreign_unchanged(self):
|
|
require(self.foreign_baseline is not None, 'foreign_snapshot_missing')
|
|
require(self.metadata_snapshot(exclude_owned=True) == self.foreign_baseline,
|
|
'foreign_docker_state_changed')
|
|
|
|
def create_volume(self, name, phase, role):
|
|
self.guard_new('volumes', name)
|
|
self.resources['volumes'][name] = {'id': None, 'phase': phase, 'kind': role}
|
|
_, stdout, _ = self.runner.docker(
|
|
'create_volume',
|
|
['volume', 'create', *self.labels(phase, role), name],
|
|
)
|
|
require(stdout.decode('ascii', errors='ignore').strip() == name, 'volume_create_result')
|
|
self.inspect_owned('volumes', name, self.resources['volumes'][name])
|
|
self.resources['volumes'][name]['metadata'] = self.volume_metadata(name)
|
|
|
|
def create_network(self, name, phase):
|
|
self.guard_new('networks', name)
|
|
self.resources['networks'][name] = {
|
|
'id': None, 'phase': phase, 'kind': 'network',
|
|
}
|
|
_, stdout, _ = self.runner.docker(
|
|
'create_network',
|
|
['network', 'create', '--driver', 'bridge', '--internal',
|
|
'--subnet', isolated_test_subnet(self.run_id, phase),
|
|
*self.labels(phase, 'network'), name],
|
|
)
|
|
identifier = stdout.decode('ascii', errors='strict').strip()
|
|
require(HEX_64.fullmatch(identifier), 'network_create_result')
|
|
self.resources['networks'][name]['id'] = identifier
|
|
self.inspect_owned('networks', name, self.resources['networks'][name])
|
|
|
|
def planned_container(self, name, phase, role):
|
|
self.resources['containers'][name] = {'id': None, 'phase': phase, 'kind': role}
|
|
|
|
def created_container(self, name, identifier):
|
|
self.resources['containers'][name]['id'] = identifier
|
|
self.inspect_owned('containers', name, self.resources['containers'][name])
|
|
|
|
def inventory(self):
|
|
inventory = {}
|
|
for kind in self.resources:
|
|
try:
|
|
inventory[kind] = sorted(self.list_names(kind, owned=True))
|
|
except Exception:
|
|
inventory[kind] = sorted(self.resources[kind])
|
|
return inventory
|
|
|
|
def guarded_stop(self):
|
|
stopped = 0
|
|
for name, expected in reversed(self.resources['containers'].items()):
|
|
try:
|
|
value = self.inspect_owned('containers', name, expected)
|
|
if expected['id'] is None:
|
|
expected['id'] = value['id']
|
|
if value['running'] or value['paused'] or value['restarting']:
|
|
value = self.inspect_owned('containers', name, expected)
|
|
code, _, _ = self.runner.docker(
|
|
'guarded_stop_owned_container',
|
|
['container', 'stop', '--time', '30', value['id']],
|
|
timeout=45, check=False,
|
|
)
|
|
stopped += int(code == 0)
|
|
except (Exception, KeyboardInterrupt):
|
|
continue
|
|
return stopped
|
|
|
|
def cleanup(self):
|
|
inventory = {
|
|
kind: self.list_names(kind, owned=True) for kind in self.resources
|
|
}
|
|
for kind, expected in self.resources.items():
|
|
require(inventory[kind] == set(expected), 'cleanup_ownership_guard')
|
|
for name, expected in reversed(self.resources['containers'].items()):
|
|
value = self.inspect_owned('containers', name, expected)
|
|
if value['running'] or value['paused'] or value['restarting']:
|
|
value = self.inspect_owned('containers', name, expected)
|
|
self.runner.docker(
|
|
'stop_owned_container', ['container', 'stop', '--time', '30', value['id']],
|
|
timeout=45,
|
|
)
|
|
value = self.inspect_owned('containers', name, expected)
|
|
self.runner.docker(
|
|
'remove_owned_container', ['container', 'rm', value['id']],
|
|
timeout=45,
|
|
)
|
|
for name, expected in reversed(self.resources['volumes'].items()):
|
|
self.inspect_owned('volumes', name, expected)
|
|
self.runner.docker('remove_owned_volume', ['volume', 'rm', name])
|
|
for name, expected in reversed(self.resources['networks'].items()):
|
|
value = self.inspect_owned('networks', name, expected)
|
|
self.runner.docker('remove_owned_network', ['network', 'rm', value['id']])
|
|
require(
|
|
all(not self.list_names(kind, owned=True) for kind in self.resources),
|
|
'cleanup_incomplete',
|
|
)
|
|
self.assert_foreign_unchanged()
|
|
|
|
|
|
class WindowsClient:
|
|
def __init__(self, command, environment, log_path):
|
|
self.command = command
|
|
self.environment = environment
|
|
self.log_path = Path(log_path)
|
|
self.process = None
|
|
self.reader = None
|
|
self.output = bytearray()
|
|
self.output_lock = threading.Lock()
|
|
self.output_exceeded = False
|
|
|
|
def _read_output(self, process):
|
|
for block in iter(lambda: process.stdout.read(64 * 1024), b''):
|
|
with self.output_lock:
|
|
if len(self.output) + len(block) <= MAX_OUTPUT:
|
|
self.output.extend(block)
|
|
else:
|
|
self.output_exceeded = True
|
|
|
|
def read_log(self):
|
|
with self.output_lock:
|
|
require(not self.output_exceeded, 'windows_log_bound')
|
|
return bytes(self.output)
|
|
|
|
def start(self):
|
|
require(self.process is None, 'windows_client_already_started')
|
|
try:
|
|
self.process = subprocess.Popen(
|
|
self.command,
|
|
cwd=os.fspath(self.log_path.parent),
|
|
env=self.environment,
|
|
stdin=subprocess.DEVNULL,
|
|
stdout=subprocess.PIPE,
|
|
stderr=subprocess.STDOUT,
|
|
creationflags=(
|
|
getattr(subprocess, 'CREATE_NEW_PROCESS_GROUP', 0)
|
|
| getattr(subprocess, 'CREATE_NO_WINDOW', 0)
|
|
),
|
|
)
|
|
except OSError:
|
|
raise Failure('windows_client_unavailable') from None
|
|
self.reader = threading.Thread(target=self._read_output, args=(self.process,), daemon=True)
|
|
self.reader.start()
|
|
|
|
def alive(self):
|
|
return self.process is not None and self.process.poll() is None
|
|
|
|
def stop(self):
|
|
if self.process is None:
|
|
return
|
|
if self.process.poll() is None:
|
|
taskkill = shutil.which('taskkill.exe') or 'taskkill.exe'
|
|
subprocess.run(
|
|
[taskkill, '/PID', str(self.process.pid), '/T', '/F'],
|
|
stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL,
|
|
stderr=subprocess.DEVNULL, check=False,
|
|
creationflags=getattr(subprocess, 'CREATE_NO_WINDOW', 0),
|
|
)
|
|
try:
|
|
self.process.wait(timeout=15)
|
|
except subprocess.TimeoutExpired:
|
|
self.process.kill()
|
|
self.process.wait(timeout=5)
|
|
if self.reader is not None:
|
|
self.reader.join(timeout=5)
|
|
if self.process.stdout is not None:
|
|
self.process.stdout.close()
|
|
if self.reader is not None:
|
|
self.reader.join(timeout=1)
|
|
self.process = None
|
|
self.reader = None
|
|
|
|
|
|
class LoopbackProxy:
|
|
def __init__(self, runner, upstream, log_path):
|
|
self.runner = runner
|
|
self.upstream = upstream
|
|
self.log_path = Path(log_path)
|
|
self.process = None
|
|
self.port = None
|
|
self.output = None
|
|
|
|
def start(self):
|
|
require(self.process is None, 'loopback_proxy_already_started')
|
|
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as reservation:
|
|
reservation.bind(('127.0.0.1', 0))
|
|
self.port = int(reservation.getsockname()[1])
|
|
try:
|
|
self.output = self.log_path.open('wb')
|
|
self.process = subprocess.Popen(
|
|
[self.runner.wsl, '-d', self.runner.docker_prefix[2], '--',
|
|
'python3', '-u', '-c', LOOPBACK_PROXY_SCRIPT,
|
|
str(self.port), self.upstream, '8443'],
|
|
cwd=os.fspath(self.runner.run_root), env=self.runner.host_env,
|
|
stdin=subprocess.DEVNULL, stdout=self.output,
|
|
stderr=subprocess.STDOUT,
|
|
creationflags=getattr(subprocess, 'CREATE_NO_WINDOW', 0),
|
|
)
|
|
except OSError:
|
|
if self.output is not None:
|
|
self.output.close()
|
|
self.output = None
|
|
raise Failure('loopback_proxy_unavailable') from None
|
|
|
|
end = min(self.runner.deadline, time.monotonic() + 60)
|
|
context = ssl.create_default_context(
|
|
cafile=os.fspath(self.runner.root / 'tests' / 'fixtures' / 'worker_tls_cert.pem'),
|
|
)
|
|
while time.monotonic() < end:
|
|
require(self.process.poll() is None, 'loopback_proxy_exited')
|
|
try:
|
|
with socket.create_connection(
|
|
('127.0.0.1', self.port), timeout=0.5,
|
|
) as connection, context.wrap_socket(
|
|
connection, server_hostname='localhost',
|
|
):
|
|
return self.port
|
|
except OSError:
|
|
time.sleep(0.1)
|
|
try:
|
|
detail = self.log_path.read_text(encoding='utf-8', errors='replace')
|
|
except OSError:
|
|
detail = ''
|
|
if 'ConnectionRefusedError' in detail:
|
|
raise Failure('loopback_proxy_upstream_refused')
|
|
if 'TimeoutError' in detail:
|
|
raise Failure('loopback_proxy_upstream_timeout')
|
|
if 'proxy upstream connected' in detail:
|
|
raise Failure('loopback_proxy_tls_timeout')
|
|
raise Failure('loopback_proxy_timeout')
|
|
|
|
def stop(self):
|
|
if self.process is not None and self.process.poll() is None:
|
|
self.process.terminate()
|
|
try:
|
|
self.process.wait(timeout=10)
|
|
except subprocess.TimeoutExpired:
|
|
self.process.kill()
|
|
self.process.wait(timeout=5)
|
|
self.process = None
|
|
if self.output is not None:
|
|
self.output.close()
|
|
self.output = None
|
|
|
|
|
|
class Verifier:
|
|
def __init__(self, args):
|
|
self.args = args
|
|
self.script = Path(__file__).resolve(strict=True)
|
|
self.root = self.script.parent.parent.resolve(strict=True)
|
|
require(self.script == self.root / 'docker' / 'verify_packaged_workers.py', 'script_path')
|
|
require('build/' in (self.root / '.gitignore').read_text(encoding='utf-8').splitlines(),
|
|
'build_not_gitignored')
|
|
self.run_id = secrets.token_hex(8)
|
|
self.prefix = 'truf-packaged-worker-e2e-' + self.run_id
|
|
build = self.root / 'build'
|
|
build.mkdir(exist_ok=True)
|
|
self.run_root = build / ('pwe-' + self.run_id)
|
|
self.run_root.mkdir()
|
|
self.deadline = time.monotonic() + args.timeout_seconds
|
|
self.runner = CommandRunner(self.root, self.run_root, args.wsl_distro, self.deadline)
|
|
self.tracker = ResourceTracker(self.runner, self.run_id)
|
|
self.windows_artifact = Path(args.windows_artifact)
|
|
if not self.windows_artifact.is_absolute():
|
|
self.windows_artifact = self.root / self.windows_artifact
|
|
self.windows_artifact = self.windows_artifact.resolve(strict=True)
|
|
require(self.windows_artifact.is_relative_to(self.root), 'windows_artifact_outside_root')
|
|
self.tokens = {
|
|
TARGETS[0]: synthetic_openai_token('a'),
|
|
TARGETS[1]: synthetic_openai_token('b'),
|
|
}
|
|
require(len(set(self.tokens.values())) == 2, 'synthetic_tokens_distinct')
|
|
self.device_tokens = {}
|
|
self.windows_client = None
|
|
self.loopback_proxy = None
|
|
self.image_details = {}
|
|
self.repository_heads = {}
|
|
self.phase_results = {}
|
|
|
|
def image(self, reference, label, *, worker=False):
|
|
value = self.runner.docker_json(label, ['image', 'inspect', reference])
|
|
require(isinstance(value, list) and len(value) == 1, label + '_shape')
|
|
details = value[0]
|
|
require(
|
|
details.get('Os') == 'linux' and details.get('Architecture') == 'amd64'
|
|
and HEX_64.fullmatch(str(details.get('Id') or '').removeprefix('sha256:')),
|
|
label + '_platform',
|
|
)
|
|
if worker:
|
|
expected = [
|
|
'/usr/bin/tini', '--', '/usr/local/bin/python3', '-u', '-I', '-S', '-B',
|
|
'/opt/truf-worker/app/remote_worker_bootstrap.py', '--',
|
|
]
|
|
require(details.get('Config', {}).get('Entrypoint') == expected,
|
|
'linux_worker_entrypoint')
|
|
require(details.get('Config', {}).get('Cmd') == ['run'],
|
|
'linux_worker_default_command')
|
|
require(details.get('Config', {}).get('User') == '10001:10001',
|
|
'linux_worker_user')
|
|
self.image_details[label] = details['Id']
|
|
|
|
def prepare_repositories(self):
|
|
git = shutil.which('git.exe') or shutil.which('git')
|
|
require(git, 'git_unavailable')
|
|
repositories = self.run_root / 'repositories'
|
|
repositories.mkdir()
|
|
base_env = dict(self.runner.host_env)
|
|
base_env.update({
|
|
'GIT_CONFIG_NOSYSTEM': '1', 'GIT_CONFIG_GLOBAL': 'NUL',
|
|
'GIT_TERMINAL_PROMPT': '0', 'TZ': 'UTC',
|
|
'GIT_AUTHOR_NAME': 'TRUF Packaged Worker Fixture',
|
|
'GIT_AUTHOR_EMAIL': 'packaged-worker-fixture@invalid.example',
|
|
'GIT_COMMITTER_NAME': 'TRUF Packaged Worker Fixture',
|
|
'GIT_COMMITTER_EMAIL': 'packaged-worker-fixture@invalid.example',
|
|
'GIT_AUTHOR_DATE': '2001-01-01T00:00:00+00:00',
|
|
'GIT_COMMITTER_DATE': '2001-01-01T00:00:00+00:00',
|
|
})
|
|
for index, target in enumerate(TARGETS):
|
|
name = chr(ord('a') + index)
|
|
path = repositories / name
|
|
path.mkdir()
|
|
write_bytes(path / 'synthetic.env', ('OPENAI_API_KEY=' + self.tokens[target] + '\n').encode('ascii'))
|
|
self.runner.execute(
|
|
'git_init_' + name,
|
|
[git, 'init', '--quiet', '--initial-branch=main', '--template=', os.fspath(path)],
|
|
env=base_env,
|
|
)
|
|
self.runner.execute(
|
|
'git_config_' + name,
|
|
[git, '-C', os.fspath(path), 'config', 'core.autocrlf', 'false'],
|
|
env=base_env,
|
|
)
|
|
self.runner.execute(
|
|
'git_add_' + name, [git, '-C', os.fspath(path), 'add', '--', 'synthetic.env'],
|
|
env=base_env,
|
|
)
|
|
self.runner.execute(
|
|
'git_commit_' + name,
|
|
[git, '-C', os.fspath(path), 'commit', '--quiet', '--no-gpg-sign',
|
|
'--message', 'Add deterministic synthetic detector fixture'],
|
|
env=base_env,
|
|
)
|
|
_, stdout, _ = self.runner.execute(
|
|
'git_head_' + name, [git, '-C', os.fspath(path), 'rev-parse', 'HEAD'],
|
|
env=base_env,
|
|
)
|
|
head = stdout.decode('ascii', errors='strict').strip()
|
|
require(HEX_40.fullmatch(head), 'fixture_commit_hash')
|
|
_, status_output, _ = self.runner.execute(
|
|
'git_status_' + name,
|
|
[git, '-C', os.fspath(path), 'status', '--porcelain=v1'], env=base_env,
|
|
)
|
|
require(not status_output, 'fixture_repository_dirty')
|
|
self.repository_heads[target] = head
|
|
require(len(set(self.repository_heads.values())) == 2, 'fixture_commits_distinct')
|
|
|
|
def prepare_fixtures(self):
|
|
manifest_path = regular_file(
|
|
self.windows_artifact / 'worker-package.json', 'windows_manifest',
|
|
)
|
|
for relative, label in (
|
|
('truf-worker.cmd', 'windows_cli_entrypoint'),
|
|
('run-worker.cmd', 'windows_entrypoint'),
|
|
('runtime/python/python.exe', 'windows_python'),
|
|
('bin/trufflehog.exe', 'windows_scanner'),
|
|
):
|
|
regular_file(self.windows_artifact / Path(relative), label, maximum=256 * 1024 * 1024)
|
|
require(
|
|
b'remote_worker_bootstrap.py" -- %*' in
|
|
(self.windows_artifact / 'truf-worker.cmd').read_bytes(),
|
|
'windows_cli_entrypoint_contents',
|
|
)
|
|
require(
|
|
b'remote_worker_bootstrap.py" -- run %*' in
|
|
(self.windows_artifact / 'run-worker.cmd').read_bytes(),
|
|
'windows_compat_entrypoint_contents',
|
|
)
|
|
windows_manifest = manifest_path.read_bytes()
|
|
windows_value = load_json_bytes(windows_manifest, 'windows_manifest')
|
|
expected_capabilities = {
|
|
('gitlab', 'gitlab', 'exact_git_v1'),
|
|
('dockerhub', 'docker', 'docker_direct_v1'),
|
|
('huggingface', 'huggingface', 'huggingface_space_v1'),
|
|
}
|
|
windows_capabilities = {
|
|
(
|
|
item.get('source'), item.get('platform'),
|
|
item.get('planning_kind'),
|
|
)
|
|
for item in windows_value.get('capabilities', [])
|
|
if isinstance(item, dict)
|
|
}
|
|
require(
|
|
windows_value.get('schema') == 3
|
|
and windows_value.get('protocol_version') == 2
|
|
and windows_value.get('platform_tag') == 'windows-x86_64',
|
|
'windows_manifest_identity',
|
|
)
|
|
require(
|
|
windows_capabilities == expected_capabilities,
|
|
'windows_manifest_capabilities',
|
|
)
|
|
helper_name = self.prefix + '-linux-manifest'
|
|
self.create_container(
|
|
helper_name, 'linux', 'manifest-reader',
|
|
['--network', 'none', '--entrypoint', '/bin/cat', self.args.linux_image,
|
|
'/opt/truf-worker/worker-package.json'],
|
|
)
|
|
_, linux_manifest, _ = self.runner.docker(
|
|
'linux_manifest',
|
|
['container', 'start', '--attach', helper_name],
|
|
)
|
|
_, exit_code, _ = self.runner.docker(
|
|
'linux_manifest_exit',
|
|
['container', 'inspect', '--format={{.State.ExitCode}}', helper_name],
|
|
)
|
|
require(exit_code.strip() == b'0', 'linux_manifest_reader_exit')
|
|
linux_value = load_json_bytes(linux_manifest, 'linux_manifest')
|
|
linux_capabilities = {
|
|
(
|
|
item.get('source'), item.get('platform'),
|
|
item.get('planning_kind'),
|
|
)
|
|
for item in linux_value.get('capabilities', [])
|
|
if isinstance(item, dict)
|
|
}
|
|
require(
|
|
linux_value.get('schema') == 3
|
|
and linux_value.get('protocol_version') == 2
|
|
and linux_value.get('platform_tag') == 'linux-x86_64',
|
|
'linux_manifest_identity',
|
|
)
|
|
require(
|
|
linux_capabilities == expected_capabilities
|
|
and linux_capabilities == windows_capabilities,
|
|
'linux_manifest_capabilities',
|
|
)
|
|
|
|
fixture = self.run_root / 'fixture'
|
|
fixture.mkdir()
|
|
write_json(fixture / 'windows-manifest.json', windows_value)
|
|
write_json(fixture / 'linux-manifest.json', linux_value)
|
|
write_json(fixture / 'fixture.json', {
|
|
'schema': 1,
|
|
'targets': list(TARGETS),
|
|
'direct_targets': DIRECT_TARGETS,
|
|
'repositories': {
|
|
target: {'head_sha': self.repository_heads[target]} for target in TARGETS
|
|
},
|
|
})
|
|
certificate = regular_file(
|
|
self.root / 'tests' / 'fixtures' / 'worker_tls_cert.pem', 'tls_certificate',
|
|
)
|
|
private_key = regular_file(
|
|
self.root / 'tests' / 'fixtures' / 'worker_tls_key.pem', 'tls_private_key',
|
|
)
|
|
decoded = ssl._ssl._test_decode_cert(os.fspath(certificate))
|
|
require(('DNS', 'localhost') in decoded.get('subjectAltName', ()), 'tls_localhost_san')
|
|
require(ssl.cert_time_to_seconds(decoded['notAfter']) > time.time(), 'tls_certificate_expired')
|
|
write_bytes(fixture / 'worker_tls_cert.pem', certificate.read_bytes())
|
|
write_bytes(fixture / 'worker_tls_key.pem', private_key.read_bytes())
|
|
self.fixture = fixture
|
|
self.manifest_hashes = {
|
|
'windows': sha256_bytes(windows_manifest),
|
|
'linux': sha256_bytes(linux_manifest),
|
|
}
|
|
|
|
def preflight(self):
|
|
require(os.name == 'nt', 'windows_host_required')
|
|
self.tracker.snapshot_foreign()
|
|
self.image(self.args.linux_image, 'linux_image', worker=True)
|
|
self.image(self.args.test_image, 'test_image')
|
|
self.prepare_repositories()
|
|
self.prepare_fixtures()
|
|
|
|
def phase_names(self, phase):
|
|
base = self.prefix + '-' + phase
|
|
return {
|
|
'network': base + '-network',
|
|
'server_data': base + '-server-data',
|
|
'server_seed': base + '-server-seed',
|
|
'fixture': base + '-fixture-data',
|
|
'worker_data': base + '-worker-data',
|
|
'fixture_seed': base + '-fixture-seed',
|
|
'worker_seed': base + '-worker-seed',
|
|
'server': base + '-server',
|
|
'worker': base + '-worker',
|
|
}
|
|
|
|
def create_container(self, name, phase, role, arguments):
|
|
self.tracker.guard_new('containers', name)
|
|
self.tracker.planned_container(name, phase, role)
|
|
_, stdout, _ = self.runner.docker(
|
|
'create_' + role,
|
|
['container', 'create', '--pull=never', '--name', name,
|
|
*self.tracker.labels(phase, role), *arguments],
|
|
)
|
|
identifier = stdout.decode('ascii', errors='ignore').strip()
|
|
require(HEX_64.fullmatch(identifier), role + '_container_id')
|
|
self.tracker.created_container(name, identifier)
|
|
|
|
def seed_volume(self, phase, role, name, volume, source):
|
|
self.create_container(
|
|
name, phase, role,
|
|
['--network', 'none', '--user', '0:0',
|
|
'--mount', f'type=volume,source={volume},target=/seed',
|
|
'--entrypoint', '/usr/local/bin/python3', self.args.test_image,
|
|
'-u', '-I', '-S', '-B', '/seed/_seed.py'],
|
|
)
|
|
source_wsl = self.runner.wsl_path(Path(source).resolve(strict=True))
|
|
self.runner.docker(
|
|
'copy_' + role, ['container', 'cp', source_wsl + '/.', name + ':/seed/'],
|
|
timeout=120,
|
|
)
|
|
self.runner.docker('start_' + role, ['container', 'start', '--attach', name], timeout=120)
|
|
_, stdout, _ = self.runner.docker(
|
|
'wait_' + role,
|
|
['container', 'inspect', '--format={{.State.ExitCode}}', name],
|
|
)
|
|
require(stdout.strip() == b'0', role + '_exit')
|
|
|
|
def prepare_phase_resources(self, phase):
|
|
names = self.phase_names(phase)
|
|
phase_root = self.run_root / phase
|
|
phase_root.mkdir()
|
|
if phase == 'windows':
|
|
write_bytes(
|
|
phase_root / 'publish-direct.py', PUBLISH_DIRECT_SCRIPT.encode('ascii'),
|
|
)
|
|
fixture_seed = phase_root / 'fixture-seed'
|
|
shutil.copytree(self.fixture, fixture_seed)
|
|
write_bytes(fixture_seed / '_seed.py', SEED_SCRIPT.encode('ascii'))
|
|
server_seed = phase_root / 'server-seed'
|
|
server_seed.mkdir()
|
|
write_bytes(server_seed / '_seed.py', SEED_SCRIPT.encode('ascii'))
|
|
self.tracker.create_network(names['network'], phase)
|
|
self.tracker.create_volume(names['server_data'], phase, 'server-data')
|
|
self.seed_volume(
|
|
phase, 'server-seed', names['server_seed'], names['server_data'], server_seed,
|
|
)
|
|
self.tracker.create_volume(names['fixture'], phase, 'fixture-data')
|
|
self.seed_volume(
|
|
phase, 'fixture-seed', names['fixture_seed'], names['fixture'], fixture_seed,
|
|
)
|
|
if phase == 'linux':
|
|
worker_seed = phase_root / 'worker-seed'
|
|
worker_seed.mkdir()
|
|
shutil.copytree(self.run_root / 'repositories', worker_seed / 'repos')
|
|
shutil.copyfile(
|
|
self.fixture / 'worker_tls_cert.pem', worker_seed / 'worker_tls_cert.pem',
|
|
)
|
|
write_bytes(worker_seed / '_seed.py', SEED_SCRIPT.encode('ascii'))
|
|
write_bytes(worker_seed / '_inspect.py', INSPECT_WORKER_SCRIPT.encode('ascii'))
|
|
write_bytes(
|
|
worker_seed / '_publish_direct.py', PUBLISH_DIRECT_SCRIPT.encode('ascii'),
|
|
)
|
|
self.tracker.create_volume(names['worker_data'], phase, 'worker-data')
|
|
self.seed_volume(
|
|
phase, 'worker-seed', names['worker_seed'], names['worker_data'], worker_seed,
|
|
)
|
|
self.device_tokens[phase] = secrets.token_hex(32)
|
|
self.start_server(phase, names)
|
|
self.wait_control(phase, names['server'], 'prepared.json', {
|
|
'schema': 1, 'phase': phase, 'target_count': 2,
|
|
}, 180)
|
|
return names, phase_root
|
|
|
|
def start_server(self, phase, names):
|
|
self.create_container(
|
|
names['server'], phase, 'server',
|
|
['--network', names['network'],
|
|
'--read-only', '--cap-drop', 'ALL', '--security-opt', 'no-new-privileges',
|
|
'--pids-limit', '512', '--stop-timeout', '30',
|
|
'--tmpfs', '/tmp:rw,nosuid,nodev,noexec,size=128m,mode=1777',
|
|
'--mount', f'type=volume,source={names["server_data"]},target=/data',
|
|
'--mount', f'type=volume,source={names["fixture"]},target=/fixture,readonly',
|
|
'--env', 'TRUF_WORKER_E2E_TOKEN=' + self.device_tokens[phase],
|
|
'--env', 'TRUF_WORKER_E2E_PHASE=' + phase,
|
|
'--entrypoint', '/usr/bin/tini', self.args.test_image,
|
|
'--', '/usr/local/bin/python3', '-u', '-I', '-S', '-B',
|
|
'/opt/truf/tests/packaged_worker_e2e_server.py'],
|
|
)
|
|
self.runner.docker('start_server', ['container', 'start', names['server']])
|
|
require(self.container_running(names['server']), 'server_not_running')
|
|
|
|
def container_running(self, name):
|
|
code, stdout, _ = self.runner.docker(
|
|
'container_running',
|
|
['container', 'inspect', '--format={{.State.Running}}', name],
|
|
check=False,
|
|
)
|
|
return code == 0 and stdout.strip() == b'true'
|
|
|
|
def read_control(self, server, name):
|
|
code, stdout, _ = self.runner.docker(
|
|
'read_control', ['container', 'exec', server, '/bin/cat', '/data/control/' + name],
|
|
check=False,
|
|
)
|
|
if code:
|
|
return None
|
|
value = load_json_bytes(stdout, 'control_' + name.replace('.', '_'))
|
|
require(isinstance(value, dict), 'control_object')
|
|
return value
|
|
|
|
def wait_until(self, label, function, seconds, *, alive=None):
|
|
end = min(self.deadline, time.monotonic() + seconds)
|
|
while time.monotonic() < end:
|
|
value = function()
|
|
if value is not None and value is not False:
|
|
return value
|
|
if alive is not None:
|
|
require(alive(), label + '_process_exited')
|
|
time.sleep(0.25)
|
|
raise Failure(label + '_timeout')
|
|
|
|
def wait_control(self, phase, server, name, expected, seconds, *, alive=None):
|
|
def marker():
|
|
value = self.read_control(server, name)
|
|
if value is None:
|
|
return None
|
|
require(value == expected, phase + '_' + name.replace('.', '_') + '_content')
|
|
return value
|
|
|
|
return self.wait_until(
|
|
phase + '_' + name.replace('.', '_'), marker, seconds,
|
|
alive=lambda: self.container_running(server) and (alive is None or alive()),
|
|
)
|
|
|
|
@staticmethod
|
|
def git_environment(local_repositories, certificate, *, linux=False):
|
|
environment = {
|
|
'SSL_CERT_FILE': os.fspath(certificate),
|
|
'GIT_ALLOW_PROTOCOL': 'file',
|
|
'GIT_TERMINAL_PROMPT': '0',
|
|
'GIT_LFS_SKIP_SMUDGE': '1',
|
|
'GIT_CONFIG_NOSYSTEM': '1',
|
|
'GIT_CONFIG_GLOBAL': '/dev/null' if linux else 'NUL',
|
|
'GIT_CONFIG_COUNT': '2',
|
|
}
|
|
for index, target in enumerate(TARGETS):
|
|
local = local_repositories[index]
|
|
uri = local if linux else Path(local).resolve(strict=True).as_uri()
|
|
environment[f'GIT_CONFIG_KEY_{index}'] = f'url.{uri}.insteadOf'
|
|
environment[f'GIT_CONFIG_VALUE_{index}'] = target
|
|
return environment
|
|
|
|
def windows_snapshot(self, phase_root):
|
|
state_root = self.run_root / 'w' / 'TRUF' / 'RemoteWorker'
|
|
bundle_root = state_root / 'bundles'
|
|
work_root = state_root / 'work'
|
|
states = {}
|
|
if state_root.exists():
|
|
for path in sorted(state_root.glob('slot-*.json')):
|
|
regular_file(path, 'windows_state', maximum=64 * 1024 * 1024)
|
|
value = load_json_bytes(path.read_bytes(), 'windows_state')
|
|
assignment = dict(value.get('assignment') or {})
|
|
reservation = dict(assignment.get('reservation') or {})
|
|
snapshot = dict(assignment.get('execution_snapshot') or {})
|
|
planning = dict(snapshot.get('planning') or {})
|
|
details = path.stat(follow_symlinks=False)
|
|
states[path.name] = {
|
|
'phase': value.get('phase'),
|
|
'reservation_id': reservation.get('reservation_id'),
|
|
'bundle_id': reservation.get('bundle_id'),
|
|
'scan_event_id': reservation.get('scan_event_id'),
|
|
'source': reservation.get('source'),
|
|
'platform': reservation.get('platform'),
|
|
'target': reservation.get('target'),
|
|
'planning_kind': planning.get('kind'),
|
|
'sha256': sha256_file(path), 'size': details.st_size,
|
|
'mtime_ns': details.st_mtime_ns,
|
|
}
|
|
bundles = {}
|
|
if bundle_root.exists():
|
|
for path in sorted(bundle_root.rglob('*.trb')):
|
|
regular_file(path, 'windows_bundle', maximum=64 * 1024 * 1024)
|
|
details = path.stat(follow_symlinks=False)
|
|
bundles[path.relative_to(bundle_root).as_posix()] = {
|
|
'sha256': sha256_file(path), 'size': details.st_size,
|
|
'mtime_ns': details.st_mtime_ns,
|
|
}
|
|
snapshot = {'states': states, 'bundles': bundles}
|
|
if not self.durable(snapshot):
|
|
return snapshot
|
|
snapshot['work'] = self.host_tree_identity(work_root)
|
|
return snapshot
|
|
|
|
@staticmethod
|
|
def host_tree_identity(root):
|
|
digest = hashlib.sha256()
|
|
count = 0
|
|
active_entries = 0
|
|
root = Path(root)
|
|
if not root.exists():
|
|
return {'count': 0, 'active_entries': 0, 'sha256': digest.hexdigest()}
|
|
for current, directories, files in os.walk(root, followlinks=False):
|
|
directories.sort()
|
|
files.sort()
|
|
for name in directories:
|
|
path = Path(current) / name
|
|
require(not path.is_symlink() and path.is_dir(), 'windows_work_directory')
|
|
if path.relative_to(root).parts[0] != 'abandoned':
|
|
active_entries += 1
|
|
for name in files:
|
|
path = regular_file(
|
|
Path(current) / name, 'windows_work_file', maximum=256 * 1024 * 1024,
|
|
)
|
|
if path.relative_to(root).parts[0] != 'abandoned':
|
|
active_entries += 1
|
|
details = path.stat(follow_symlinks=False)
|
|
digest.update(path.relative_to(root).as_posix().encode('utf-8') + b'\0')
|
|
digest.update(str(details.st_size).encode('ascii') + b'\0')
|
|
digest.update(str(details.st_mtime_ns).encode('ascii') + b'\0')
|
|
digest.update(sha256_file(path).encode('ascii') + b'\n')
|
|
count += 1
|
|
return {
|
|
'count': count, 'active_entries': active_entries,
|
|
'sha256': digest.hexdigest(),
|
|
}
|
|
|
|
def linux_snapshot(self, worker):
|
|
code, stdout, _ = self.runner.docker(
|
|
'linux_worker_snapshot',
|
|
['container', 'exec', worker, '/usr/local/bin/python3',
|
|
'-u', '-I', '-S', '-B', '/data/_inspect.py'],
|
|
check=False,
|
|
)
|
|
if code:
|
|
return None
|
|
value = load_json_bytes(stdout, 'linux_worker_snapshot')
|
|
require(isinstance(value, dict), 'linux_snapshot_object')
|
|
return value
|
|
|
|
@staticmethod
|
|
def durable(snapshot):
|
|
if not isinstance(snapshot, dict):
|
|
return False
|
|
states = snapshot.get('states')
|
|
bundles = snapshot.get('bundles')
|
|
if set(states or {}) != {'slot-0.json', 'slot-1.json'} or len(bundles or {}) != 2:
|
|
return False
|
|
bundle_ids = set()
|
|
for state in states.values():
|
|
if (
|
|
state.get('phase') != 'bundle_ready'
|
|
or not HEX_64.fullmatch(str(state.get('sha256') or ''))
|
|
or not re.fullmatch(r'[a-f0-9]{32,64}', str(state.get('bundle_id') or ''))
|
|
or int(state.get('reservation_id') or 0) <= 0
|
|
):
|
|
return False
|
|
bundle_ids.add(state['bundle_id'])
|
|
if len(bundle_ids) != 2:
|
|
return False
|
|
for path, entry in bundles.items():
|
|
if (
|
|
Path(path).name.removesuffix('.trb') not in bundle_ids
|
|
or not HEX_64.fullmatch(str(entry.get('sha256') or ''))
|
|
or int(entry.get('size') or 0) <= 0
|
|
):
|
|
return False
|
|
return True
|
|
|
|
@staticmethod
|
|
def direct_assigned(snapshot, bundle_count):
|
|
if not isinstance(snapshot, dict):
|
|
return False
|
|
states = snapshot.get('states')
|
|
bundles = snapshot.get('bundles')
|
|
if (
|
|
set(states or {}) != {'slot-0.json', 'slot-1.json'}
|
|
or len(bundles or {}) != bundle_count
|
|
):
|
|
return False
|
|
expected = {
|
|
('dockerhub', 'docker', 'docker_direct_v1', DOCKER_TARGET),
|
|
('huggingface', 'huggingface', 'huggingface_space_v1', HUGGINGFACE_TARGET),
|
|
}
|
|
identities = set()
|
|
bundle_ids = set()
|
|
for state in states.values():
|
|
if (
|
|
state.get('phase') != 'assigned'
|
|
or not HEX_64.fullmatch(str(state.get('sha256') or ''))
|
|
or not re.fullmatch(r'[a-f0-9]{32,64}', str(state.get('bundle_id') or ''))
|
|
or int(state.get('reservation_id') or 0) <= 0
|
|
or not re.fullmatch(r'[a-f0-9]{32,64}', str(state.get('scan_event_id') or ''))
|
|
):
|
|
return False
|
|
identities.add((
|
|
state.get('source'), state.get('platform'),
|
|
state.get('planning_kind'), state.get('target'),
|
|
))
|
|
bundle_ids.add(state['bundle_id'])
|
|
if identities != expected or len(bundle_ids) != 2:
|
|
return False
|
|
for path, entry in bundles.items():
|
|
if (
|
|
Path(path).name.removesuffix('.trb') not in bundle_ids
|
|
or not HEX_64.fullmatch(str(entry.get('sha256') or ''))
|
|
or int(entry.get('size') or 0) <= 0
|
|
):
|
|
return False
|
|
work = snapshot.get('work')
|
|
if work is not None and int(work.get('active_entries') or 0) != 0:
|
|
return False
|
|
return True
|
|
|
|
@staticmethod
|
|
def resolved_client_storage(snapshot):
|
|
if not isinstance(snapshot, dict) or snapshot.get('bundles'):
|
|
return False
|
|
work = snapshot.get('work') or {}
|
|
if int(work.get('active_entries') or 0) != 0:
|
|
return False
|
|
for state in (snapshot.get('states') or {}).values():
|
|
if (
|
|
state.get('phase') != 'claiming'
|
|
or state.get('reservation_id') is not None
|
|
or state.get('bundle_id') is not None
|
|
or state.get('scan_event_id') is not None
|
|
):
|
|
return False
|
|
return True
|
|
|
|
def assert_logs_safe(self, logs):
|
|
forbidden = [*self.tokens.values(), *self.device_tokens.values()]
|
|
for value in logs:
|
|
require(len(value) <= MAX_OUTPUT, 'log_output_bound')
|
|
for secret in forbidden:
|
|
require(secret.encode('ascii') not in value, 'secret_present_in_log')
|
|
|
|
def docker_logs(self, container):
|
|
_, stdout, stderr = self.runner.docker(
|
|
'docker_logs', ['container', 'logs', container], check=False,
|
|
)
|
|
return stdout + (b'\n' if stdout and stderr else b'') + stderr
|
|
|
|
def assert_restart_stable(self, phase, snapshot, expected, alive):
|
|
started = time.monotonic()
|
|
end = min(self.deadline, time.monotonic() + 3)
|
|
# The expected snapshot was captured before the real process restart.
|
|
checks = 1
|
|
current = None
|
|
while time.monotonic() < end:
|
|
require(alive(), phase + '_restart_process_exited')
|
|
current = snapshot()
|
|
require(
|
|
isinstance(current, dict)
|
|
and current.get('states') == expected.get('states')
|
|
and current.get('bundles') == expected.get('bundles')
|
|
and int((current.get('work') or {}).get('count') or 0)
|
|
<= int((expected.get('work') or {}).get('count') or 0),
|
|
phase + '_restart_rescanned_or_rewrote_bundle',
|
|
)
|
|
require(alive(), phase + '_restart_process_exited')
|
|
checks += 1
|
|
time.sleep(0.25)
|
|
require(
|
|
checks >= 2 and time.monotonic() - started >= 2.5,
|
|
phase + '_restart_stability_window',
|
|
)
|
|
return current
|
|
|
|
def restore_and_wait_direct(self, phase, names, snapshot, alive):
|
|
self.runner.docker(
|
|
phase + '_restore',
|
|
['container', 'exec', names['server'], '/usr/bin/touch', '/data/control/restore'],
|
|
)
|
|
return self.wait_until(
|
|
phase + '_direct_claims',
|
|
lambda: (
|
|
value if self.direct_assigned(value := snapshot(), 0) else None
|
|
),
|
|
240,
|
|
alive=lambda: self.container_running(names['server']) and alive(),
|
|
)
|
|
|
|
def publish_windows_direct(self, phase_root):
|
|
state_root = self.run_root / 'w' / 'TRUF' / 'RemoteWorker'
|
|
environment = dict(self.runner.host_env)
|
|
environment.update({
|
|
'LOCALAPPDATA': os.fspath(phase_root / 'localappdata'),
|
|
'TEMP': os.fspath(phase_root / 'temp'),
|
|
'TMP': os.fspath(phase_root / 'temp'),
|
|
})
|
|
_, stdout, stderr = self.runner.execute(
|
|
'windows_publish_direct',
|
|
[
|
|
os.fspath(self.windows_artifact / 'runtime' / 'python' / 'python.exe'),
|
|
'-I', '-S', '-B', os.fspath(phase_root / 'publish-direct.py'),
|
|
os.fspath(self.windows_artifact), os.fspath(state_root),
|
|
os.fspath(state_root / 'bundles'),
|
|
],
|
|
timeout=120,
|
|
env=environment,
|
|
)
|
|
require(
|
|
stdout.strip() == b'published direct bundles: 2' and not stderr,
|
|
'windows_direct_publish_output',
|
|
)
|
|
|
|
def publish_linux_direct(self, worker):
|
|
_, stdout, stderr = self.runner.docker(
|
|
'linux_publish_direct',
|
|
[
|
|
'container', 'exec', worker, '/usr/local/bin/python3',
|
|
'-u', '-I', '-S', '-B', '/data/_publish_direct.py',
|
|
'/opt/truf-worker', '/data/state-base/truf/remote-worker',
|
|
'/data/client/truf/remote-worker/bundles',
|
|
],
|
|
timeout=120,
|
|
)
|
|
require(
|
|
stdout.strip() == b'published direct bundles: 2' and not stderr,
|
|
'linux_direct_publish_output',
|
|
)
|
|
|
|
def start_windows_client(self, phase_root, port):
|
|
localappdata = self.run_root / 'w'
|
|
temporary = phase_root / 'temp'
|
|
localappdata.mkdir()
|
|
temporary.mkdir()
|
|
system_keys = (
|
|
'PATH', 'PATHEXT', 'SystemRoot', 'SYSTEMROOT', 'WINDIR', 'COMSPEC',
|
|
'NUMBER_OF_PROCESSORS', 'PROCESSOR_ARCHITECTURE',
|
|
)
|
|
environment = {
|
|
key: os.environ[key] for key in system_keys if key in os.environ
|
|
}
|
|
environment.update({
|
|
'LOCALAPPDATA': os.fspath(localappdata),
|
|
'TEMP': os.fspath(temporary), 'TMP': os.fspath(temporary),
|
|
**self.git_environment(
|
|
[self.run_root / 'repositories' / 'a', self.run_root / 'repositories' / 'b'],
|
|
self.fixture / 'worker_tls_cert.pem',
|
|
),
|
|
})
|
|
command = [
|
|
environment.get('COMSPEC') or 'cmd.exe', '/d', '/c',
|
|
os.fspath(self.windows_artifact / 'run-worker.cmd'),
|
|
'--server', f'https://localhost:{port}',
|
|
'--token', self.device_tokens['windows'],
|
|
'--parallelism', str(PARALLELISM),
|
|
]
|
|
self.windows_client = WindowsClient(command, environment, phase_root / 'client.log')
|
|
self.windows_client.start()
|
|
|
|
def windows_phase(self):
|
|
names, phase_root = self.prepare_phase_resources('windows')
|
|
_, stdout, _ = self.runner.docker(
|
|
'windows_server_address',
|
|
['container', 'inspect',
|
|
'--format={{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}',
|
|
names['server']],
|
|
)
|
|
address = stdout.decode('ascii', errors='strict').strip()
|
|
require(re.fullmatch(r'(?:[0-9]{1,3}\.){3}[0-9]{1,3}', address),
|
|
'windows_server_address')
|
|
self.loopback_proxy = LoopbackProxy(
|
|
self.runner, address, phase_root / 'loopback-proxy.log',
|
|
)
|
|
port = self.loopback_proxy.start()
|
|
self.start_windows_client(phase_root, port)
|
|
try:
|
|
self.wait_control('windows', names['server'], 'outage.json', {
|
|
'schema': 1, 'claim_count': 2, 'phase': 'windows',
|
|
}, 240, alive=self.windows_client.alive)
|
|
before = self.wait_until(
|
|
'windows_durable_bundle',
|
|
lambda: (snapshot if self.durable(snapshot := self.windows_snapshot(phase_root)) else None),
|
|
240, alive=self.windows_client.alive,
|
|
)
|
|
self.windows_client.stop()
|
|
self.windows_client.start()
|
|
self.assert_restart_stable(
|
|
'windows', lambda: self.windows_snapshot(phase_root), before,
|
|
self.windows_client.alive,
|
|
)
|
|
snapshot = lambda: self.windows_snapshot(phase_root)
|
|
self.restore_and_wait_direct(
|
|
'windows', names, snapshot, self.windows_client.alive,
|
|
)
|
|
self.publish_windows_direct(phase_root)
|
|
direct = self.wait_until(
|
|
'windows_direct_bundles',
|
|
lambda: (
|
|
value if self.direct_assigned(value := snapshot(), 2) else None
|
|
),
|
|
120,
|
|
alive=self.windows_client.alive,
|
|
)
|
|
self.windows_client.stop()
|
|
self.windows_client.start()
|
|
self.assert_restart_stable(
|
|
'windows', snapshot, direct, self.windows_client.alive,
|
|
)
|
|
self.complete_after_direct_ready('windows', names, snapshot)
|
|
return self.finish_phase('windows', names, phase_root, (before, direct))
|
|
finally:
|
|
if self.windows_client is not None:
|
|
self.windows_client.stop()
|
|
if self.loopback_proxy is not None:
|
|
self.loopback_proxy.stop()
|
|
|
|
def create_linux_worker(self, names):
|
|
environment = self.git_environment(
|
|
['file:///data/repos/a', 'file:///data/repos/b'],
|
|
'/data/worker_tls_cert.pem', linux=True,
|
|
)
|
|
arguments = [
|
|
'--network', 'container:' + names['server'],
|
|
'--read-only', '--cap-drop', 'ALL', '--security-opt', 'no-new-privileges',
|
|
'--pids-limit', '256', '--stop-timeout', '45',
|
|
'--tmpfs', '/tmp:rw,nosuid,nodev,noexec,size=128m,mode=1777',
|
|
'--mount', f'type=volume,source={names["worker_data"]},target=/data',
|
|
]
|
|
for key, value in environment.items():
|
|
arguments.extend(['--env', key + '=' + value])
|
|
arguments.extend([
|
|
'--env', 'XDG_DATA_HOME=/data/client', '--env', 'XDG_STATE_HOME=/data/state-base',
|
|
self.args.linux_image,
|
|
'--server', 'https://localhost:8443', '--token', self.device_tokens['linux'],
|
|
'--parallelism', str(PARALLELISM),
|
|
])
|
|
self.create_container(names['worker'], 'linux', 'worker', arguments)
|
|
self.runner.docker('start_linux_worker', ['container', 'start', names['worker']])
|
|
require(self.container_running(names['worker']), 'linux_worker_not_running')
|
|
|
|
def linux_phase(self):
|
|
names, phase_root = self.prepare_phase_resources('linux')
|
|
self.create_linux_worker(names)
|
|
self.wait_control('linux', names['server'], 'outage.json', {
|
|
'schema': 1, 'claim_count': 2, 'phase': 'linux',
|
|
}, 240, alive=lambda: self.container_running(names['worker']))
|
|
before = self.wait_until(
|
|
'linux_durable_bundle',
|
|
lambda: (snapshot if self.durable(snapshot := self.linux_snapshot(names['worker'])) else None),
|
|
240, alive=lambda: self.container_running(names['worker']),
|
|
)
|
|
worker = self.tracker.inspect_owned(
|
|
'containers', names['worker'], self.tracker.resources['containers'][names['worker']],
|
|
)
|
|
self.runner.docker(
|
|
'stop_linux_worker', ['container', 'stop', '--time', '45', worker['id']],
|
|
timeout=60,
|
|
)
|
|
require(not self.container_running(names['worker']), 'linux_worker_stop')
|
|
self.runner.docker('restart_linux_worker', ['container', 'start', names['worker']])
|
|
self.assert_restart_stable(
|
|
'linux', lambda: self.linux_snapshot(names['worker']), before,
|
|
lambda: self.container_running(names['worker']),
|
|
)
|
|
snapshot = lambda: self.linux_snapshot(names['worker'])
|
|
alive = lambda: self.container_running(names['worker'])
|
|
self.restore_and_wait_direct('linux', names, snapshot, alive)
|
|
self.publish_linux_direct(names['worker'])
|
|
direct = self.wait_until(
|
|
'linux_direct_bundles',
|
|
lambda: (
|
|
value if self.direct_assigned(value := snapshot(), 2) else None
|
|
),
|
|
120,
|
|
alive=alive,
|
|
)
|
|
worker = self.tracker.inspect_owned(
|
|
'containers', names['worker'], self.tracker.resources['containers'][names['worker']],
|
|
)
|
|
self.runner.docker(
|
|
'stop_linux_direct_worker',
|
|
['container', 'stop', '--time', '45', worker['id']],
|
|
timeout=60,
|
|
)
|
|
require(not self.container_running(names['worker']), 'linux_direct_worker_stop')
|
|
self.runner.docker('restart_linux_direct_worker', ['container', 'start', names['worker']])
|
|
self.assert_restart_stable('linux_direct', snapshot, direct, alive)
|
|
self.complete_after_direct_ready('linux', names, snapshot)
|
|
return self.finish_phase('linux', names, phase_root, (before, direct))
|
|
|
|
def complete_after_direct_ready(self, phase, names, snapshot):
|
|
self.runner.docker(
|
|
phase + '_direct_ready',
|
|
['container', 'exec', names['server'], '/usr/bin/touch',
|
|
'/data/control/direct-ready'],
|
|
)
|
|
|
|
def completed():
|
|
return self.read_control(names['server'], 'completed.json')
|
|
|
|
try:
|
|
evidence = self.wait_until(
|
|
phase + '_completed', completed, 240,
|
|
alive=lambda: self.container_running(names['server']),
|
|
)
|
|
except Failure:
|
|
waiting = self.read_control(names['server'], 'completion-wait.json')
|
|
if waiting is not None:
|
|
require(
|
|
set(waiting) == {'schema', 'phase', 'reason'}
|
|
and waiting['schema'] == 1 and waiting['phase'] == phase
|
|
and isinstance(waiting['reason'], str)
|
|
and re.fullmatch(r'[A-Za-z0-9 _-]{1,80}', waiting['reason']),
|
|
phase + '_completion_wait_shape',
|
|
)
|
|
print(phase + ' completion wait: ' + waiting['reason'], flush=True)
|
|
raise
|
|
|
|
def clean():
|
|
value = snapshot()
|
|
if self.resolved_client_storage(value):
|
|
return value
|
|
return None
|
|
|
|
self.wait_until(phase + '_client_cleanup', clean, 120)
|
|
self.phase_results[phase] = evidence
|
|
|
|
def finish_phase(self, phase, names, phase_root, restart_snapshots):
|
|
if phase == 'linux':
|
|
worker = self.tracker.inspect_owned(
|
|
'containers', names['worker'], self.tracker.resources['containers'][names['worker']],
|
|
)
|
|
self.runner.docker(
|
|
'final_stop_linux_worker',
|
|
['container', 'stop', '--time', '45', worker['id']], timeout=60,
|
|
)
|
|
final, stopped_reader = self.linux_snapshot_stopped(
|
|
names['worker'], names['worker_data'], phase,
|
|
)
|
|
else:
|
|
self.windows_client.stop()
|
|
final = self.windows_snapshot(phase_root)
|
|
require(self.resolved_client_storage(final), phase + '_final_cleanup')
|
|
if phase == 'linux':
|
|
receipt = final.get('shutdown_receipt')
|
|
require(
|
|
isinstance(receipt, dict)
|
|
and set(receipt) == {
|
|
'schema', 'instance_id', 'completed_at', 'exit_code',
|
|
'drained', 'last_sequence',
|
|
}
|
|
and receipt.get('schema') == 1
|
|
and receipt.get('drained') is True
|
|
and receipt.get('exit_code') == 0,
|
|
'linux_clean_shutdown_receipt',
|
|
)
|
|
logs = []
|
|
for role in ('server_seed', 'fixture_seed', 'server'):
|
|
logs.append(self.docker_logs(names[role]))
|
|
if phase == 'linux':
|
|
logs.extend([
|
|
self.docker_logs(names['worker_seed']), self.docker_logs(names['worker']),
|
|
self.docker_logs(stopped_reader),
|
|
])
|
|
else:
|
|
logs.append(self.windows_client.read_log())
|
|
self.assert_logs_safe(logs)
|
|
return {
|
|
'bundle_sha256': sorted(
|
|
entry['sha256']
|
|
for snapshot in restart_snapshots
|
|
for entry in snapshot['bundles'].values()
|
|
),
|
|
'evidence': self.normalize_evidence(phase, self.phase_results[phase]),
|
|
}
|
|
|
|
def linux_snapshot_stopped(self, worker, volume, phase):
|
|
reader = self.prefix + '-' + phase + '-stopped-reader'
|
|
self.create_container(
|
|
reader, phase, 'stopped-reader',
|
|
['--network', 'none', '--read-only',
|
|
'--mount', f'type=volume,source={volume},target=/data',
|
|
'--entrypoint', '/usr/local/bin/python3', self.args.linux_image,
|
|
'-u', '-I', '-S', '-B', '/data/_inspect.py'],
|
|
)
|
|
_, stdout, _ = self.runner.docker(
|
|
'run_stopped_reader', ['container', 'start', '--attach', reader], timeout=60,
|
|
)
|
|
value = load_json_bytes(stdout, 'stopped_reader')
|
|
require(isinstance(value, dict), 'stopped_reader_object')
|
|
return value, reader
|
|
|
|
def normalize_evidence(self, phase, value):
|
|
expected_keys = {
|
|
'schema', 'phase', 'counts', 'claim_count', 'detectors',
|
|
'candidate_services', 'secret_hashes', 'commit_hashes',
|
|
'receipt_count', 'source_counts', 'planning_counts', 'capacity',
|
|
}
|
|
require(isinstance(value, dict) and set(value) == expected_keys, phase + '_evidence_shape')
|
|
require(
|
|
value['schema'] == 1 and value['phase'] == phase
|
|
and value['claim_count'] == value['receipt_count'] == 4
|
|
and value['detectors'] == ['OpenAI']
|
|
and value['candidate_services'] == ['openai']
|
|
and value['source_counts'] == {
|
|
'gitlab': 2, 'dockerhub': 1, 'huggingface': 1,
|
|
}
|
|
and value['planning_counts'] == {
|
|
'exact_git_v1': 2,
|
|
'docker_direct_v1': 1,
|
|
'huggingface_space_v1': 1,
|
|
},
|
|
phase + '_evidence_identity',
|
|
)
|
|
expected_secret_hashes = sorted(
|
|
sha256_bytes(token.encode('ascii')) for token in self.tokens.values()
|
|
)
|
|
require(value['secret_hashes'] == expected_secret_hashes, phase + '_secret_hashes')
|
|
require(
|
|
value['commit_hashes'] == sorted(self.repository_heads.values()),
|
|
phase + '_commit_hashes',
|
|
)
|
|
normalized = dict(value)
|
|
normalized.pop('phase')
|
|
return normalized
|
|
|
|
def clear_run_artifacts(self):
|
|
require(
|
|
self.run_root.parent == self.root / 'build'
|
|
and re.fullmatch(r'pwe-[a-f0-9]{16}', self.run_root.name)
|
|
and self.run_root.is_dir() and not self.run_root.is_symlink(),
|
|
'run_root_guard',
|
|
)
|
|
|
|
def retry_writable(function, name, _error):
|
|
details = os.lstat(name)
|
|
if stat.S_ISLNK(details.st_mode):
|
|
function(name)
|
|
return
|
|
mode = stat.S_IRUSR | stat.S_IWUSR
|
|
if stat.S_ISDIR(details.st_mode):
|
|
mode |= stat.S_IXUSR
|
|
os.chmod(name, mode)
|
|
function(name)
|
|
|
|
for path in self.run_root.iterdir():
|
|
if path.is_symlink() or path.is_file():
|
|
try:
|
|
path.unlink()
|
|
except PermissionError:
|
|
require(not path.is_symlink(), 'run_artifact_symlink_permission')
|
|
os.chmod(path, stat.S_IRUSR | stat.S_IWUSR)
|
|
path.unlink()
|
|
elif path.is_dir():
|
|
shutil.rmtree(path, onerror=retry_writable)
|
|
else:
|
|
raise Failure('run_artifact_type')
|
|
require(not any(self.run_root.iterdir()), 'run_artifact_cleanup')
|
|
|
|
def safe_evidence(self, value):
|
|
content = json.dumps(
|
|
value, ensure_ascii=True, sort_keys=True, separators=(',', ':'),
|
|
).encode('ascii') + b'\n'
|
|
require(len(content) <= MAX_SAFE_EVIDENCE, 'safe_evidence_bound')
|
|
for secret in (*self.tokens.values(), *self.device_tokens.values()):
|
|
require(secret.encode('ascii') not in content, 'secret_present_in_evidence')
|
|
return content
|
|
|
|
def retain_safe_evidence(self, name, value):
|
|
content = self.safe_evidence(value)
|
|
self.clear_run_artifacts()
|
|
write_bytes(self.run_root / name, content)
|
|
|
|
def write_summary(self, windows, linux, cleanup):
|
|
summary = {
|
|
'schema': 1, 'status': 'passed', 'run_id': self.run_id,
|
|
'parallelism': PARALLELISM, 'targets': list(ALL_TARGETS),
|
|
'repository_heads': self.repository_heads,
|
|
'manifest_sha256': self.manifest_hashes,
|
|
'image_ids': self.image_details,
|
|
'bundle_sha256': {
|
|
'windows': windows['bundle_sha256'], 'linux': linux['bundle_sha256'],
|
|
},
|
|
'normalized_evidence_sha256': sha256_bytes(json.dumps(
|
|
windows['evidence'], ensure_ascii=True, sort_keys=True,
|
|
separators=(',', ':'),
|
|
).encode('ascii')),
|
|
'foreign_docker_state': 'unchanged',
|
|
'cleanup': cleanup,
|
|
}
|
|
self.retain_safe_evidence('summary.json', summary)
|
|
return summary
|
|
|
|
def run(self):
|
|
self.preflight()
|
|
windows = self.windows_phase()
|
|
linux = self.linux_phase()
|
|
require(windows['evidence'] == linux['evidence'], 'cross_platform_evidence_mismatch')
|
|
cleanup = 'retained' if self.args.keep else 'complete'
|
|
if not self.args.keep:
|
|
self.tracker.cleanup()
|
|
else:
|
|
self.tracker.assert_foreign_unchanged()
|
|
return self.write_summary(windows, linux, cleanup)
|
|
|
|
|
|
def parse_args(argv=None):
|
|
parser = argparse.ArgumentParser(
|
|
description='Verify real packaged Windows and Linux workers across an API outage.',
|
|
)
|
|
parser.add_argument('--windows-artifact', default=WINDOWS_ARTIFACT,
|
|
help='portable Windows worker directory (default: %(default)s)')
|
|
parser.add_argument('--linux-image', default=LINUX_IMAGE,
|
|
help='already-built Linux worker image (default: %(default)s)')
|
|
parser.add_argument('--test-image', default=TEST_IMAGE,
|
|
help='already-built server/test image (default: %(default)s)')
|
|
parser.add_argument('--wsl-distro', default='Ubuntu-24.04',
|
|
help='WSL distribution that owns the Docker socket (default: %(default)s)')
|
|
parser.add_argument('--timeout-seconds', type=int, default=1800,
|
|
help='aggregate subprocess and test timeout (default: %(default)s)')
|
|
parser.add_argument('--keep', action='store_true',
|
|
help='retain labelled Docker resources after a successful run')
|
|
args = parser.parse_args(argv)
|
|
if not 300 <= args.timeout_seconds <= 7200:
|
|
parser.error('--timeout-seconds must be between 300 and 7200')
|
|
for name in ('linux_image', 'test_image', 'wsl_distro'):
|
|
if not re.fullmatch(r'[A-Za-z0-9][A-Za-z0-9._:/@+-]{0,255}', getattr(args, name)):
|
|
parser.error('--' + name.replace('_', '-') + ' contains unsupported characters')
|
|
return args
|
|
|
|
|
|
def main(argv=None):
|
|
verifier = None
|
|
try:
|
|
verifier = Verifier(parse_args(argv))
|
|
summary = verifier.run()
|
|
except KeyboardInterrupt as exc:
|
|
label = 'interrupted'
|
|
failure_class = type(exc).__name__
|
|
exit_code = None
|
|
except Failure as exc:
|
|
label = str(exc)
|
|
failure_class = type(exc).__name__
|
|
exit_code = getattr(exc, 'exit_code', None)
|
|
except Exception as exc:
|
|
label = 'unexpected_exception'
|
|
failure_class = type(exc).__name__
|
|
exit_code = None
|
|
else:
|
|
print(json.dumps({
|
|
'status': summary['status'],
|
|
'artifacts': os.fspath(verifier.run_root),
|
|
'cleanup': summary['cleanup'],
|
|
}, ensure_ascii=True, sort_keys=True))
|
|
return 0
|
|
finally:
|
|
if verifier is not None and verifier.windows_client is not None:
|
|
verifier.windows_client.stop()
|
|
if verifier is not None and verifier.loopback_proxy is not None:
|
|
verifier.loopback_proxy.stop()
|
|
if verifier is not None:
|
|
verifier.runner.deadline = time.monotonic() + 720
|
|
verifier.tracker.guarded_stop()
|
|
if not re.fullmatch(r'[a-z0-9_-]{1,160}', label):
|
|
label = 'verifier_failure'
|
|
if not re.fullmatch(r'[A-Za-z][A-Za-z0-9_]{0,79}', failure_class):
|
|
failure_class = 'Exception'
|
|
if type(exit_code) is not int or not -(2 ** 31) <= exit_code < 2 ** 31:
|
|
exit_code = None
|
|
failure = {
|
|
'stage': label, 'class': failure_class, 'exit_code': exit_code,
|
|
}
|
|
if verifier is not None:
|
|
try:
|
|
if verifier.args.keep:
|
|
write_bytes(
|
|
verifier.run_root / 'failure.json',
|
|
verifier.safe_evidence(failure),
|
|
)
|
|
else:
|
|
verifier.retain_safe_evidence('failure.json', failure)
|
|
except Exception:
|
|
pass
|
|
print(json.dumps(failure, ensure_ascii=True, sort_keys=True), file=sys.stderr)
|
|
return 1
|
|
|
|
|
|
if __name__ == '__main__':
|
|
raise SystemExit(main())
|