477 lines
22 KiB
Python
477 lines
22 KiB
Python
import os
|
|
from pathlib import Path
|
|
import runpy
|
|
from types import SimpleNamespace
|
|
import sys
|
|
import tempfile
|
|
import unittest
|
|
from unittest import mock
|
|
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
APP_DIR = ROOT / 'app'
|
|
sys.path.insert(0, str(APP_DIR))
|
|
|
|
import console_runner
|
|
import db_backend
|
|
import lifecycle_authority
|
|
import supervisor
|
|
import uvicorn
|
|
import worker_api
|
|
import worker_assignment
|
|
import managed_files
|
|
from runtime_security import ensure_private_directory
|
|
|
|
|
|
class ConfiguredWorkerServiceTests(unittest.TestCase):
|
|
def _config(self, bundle_root, **worker_overrides):
|
|
worker = {
|
|
'enabled': True,
|
|
'address': '127.0.0.1',
|
|
'port': 8766,
|
|
'sources': ['gitlab'],
|
|
'auth_entries': {'gitlab': 'selected'},
|
|
'compatibility_profiles': {'windows-x86_64': {'fixture': True}},
|
|
'assignment_ttl_seconds': 86400,
|
|
'assignment_ttl_seconds_by_source': {},
|
|
'max_bundle_bytes': 64 * 1024 * 1024,
|
|
'reaper_interval_seconds': 60,
|
|
'reaper_batch_size': 1000,
|
|
'limit_concurrency': 64,
|
|
}
|
|
worker.update(worker_overrides)
|
|
return {
|
|
'global': {
|
|
'database_url': 'postgresql://fixture',
|
|
'result_bundle_dir': bundle_root,
|
|
'result_bundle_max_event_bytes': 64 * 1024 * 1024,
|
|
'remote_assignment_reserve_bytes': 2 * 1024 * 1024,
|
|
'remote_assignment_max_active': 50,
|
|
'result_bundle_max_total_bytes': 512 * 1024 * 1024,
|
|
'projection_backlog_max_bytes': 256 * 1024 * 1024,
|
|
'projection_backlog_headroom_bytes': 128 * 1024 * 1024,
|
|
'keycheck_queue_max_items': 131072,
|
|
'keycheck_queue_max_bytes': 128 * 1024 * 1024,
|
|
'keycheck_candidates_per_event': 2000,
|
|
'keycheck_candidate_bytes_per_event': 2 * 1024 * 1024,
|
|
},
|
|
'supervisor': {'worker_api': worker},
|
|
'sources': {'gitlab': {'enabled': True}},
|
|
}
|
|
|
|
def _patch_config(self, config):
|
|
args = SimpleNamespace(
|
|
platform='gitlab', exact_git_planning_enabled=True, timeout=1800,
|
|
)
|
|
return (
|
|
mock.patch.object(console_runner, 'load_config', return_value=config),
|
|
mock.patch.object(console_runner, 'apply_global_config'),
|
|
mock.patch.object(console_runner, 'load_secrets', return_value={'fixture': True}),
|
|
mock.patch.object(console_runner, 'auth_pool_entries', return_value=([
|
|
{'name': 'other', 'token': 'not-selected'},
|
|
{'name': 'selected', 'token': 'selected-secret'},
|
|
], None)),
|
|
mock.patch.object(console_runner, 'build_args_from_source_config', return_value=args),
|
|
mock.patch.object(db_backend, 'database_url_from_env', return_value='postgresql://fixture'),
|
|
mock.patch.object(worker_assignment, 'RemoteAssignmentBuilder'),
|
|
)
|
|
|
|
def test_factory_is_disabled_by_default(self):
|
|
config = {'global': {}, 'supervisor': {}, 'sources': {}}
|
|
with mock.patch.object(console_runner, 'load_config', return_value=config):
|
|
with self.assertRaisesRegex(ValueError, 'not explicitly enabled'):
|
|
worker_api.build_configured_worker_service('config.yaml', {'instance_id': 'fixture'})
|
|
|
|
def test_factory_rejects_public_or_unspecified_bind_before_database_access(self):
|
|
config = self._config('unused', address='0.0.0.0')
|
|
with mock.patch.object(console_runner, 'load_config', return_value=config), \
|
|
mock.patch.object(db_backend, 'database_url_from_env') as database_url:
|
|
with self.assertRaisesRegex(ValueError, 'loopback or private'):
|
|
worker_api.build_configured_worker_service(
|
|
'config.yaml', {'instance_id': 'fixture'},
|
|
)
|
|
database_url.assert_not_called()
|
|
|
|
def test_factory_uses_canonical_dsn_and_explicit_auth_entry(self):
|
|
with tempfile.TemporaryDirectory() as temp_dir:
|
|
bundle_root = ensure_private_directory(
|
|
os.path.join(temp_dir, 'bundles'), reject_reparse=True,
|
|
)
|
|
config = self._config(
|
|
bundle_root,
|
|
assignment_ttl_seconds_by_source={'gitlab': 3660},
|
|
)
|
|
patches = self._patch_config(config)
|
|
with patches[0] as load_config, patches[1] as apply_global, \
|
|
patches[2] as load_secrets, patches[3], patches[4] as build_args, \
|
|
patches[5], patches[6] as builder:
|
|
service, runtime = worker_api.build_configured_worker_service(
|
|
'config.yaml', {'instance_id': 'supervisor-fixture'}, db_factory=object,
|
|
)
|
|
|
|
load_config.assert_called_once_with(
|
|
'config.yaml', managed_postgres=True, final_cutover=True,
|
|
)
|
|
apply_global.assert_called_once_with(config['global'])
|
|
load_secrets.assert_called_once_with(config, 'config.yaml')
|
|
self.assertEqual(
|
|
build_args.call_args.kwargs['auth_entry']['token'], 'selected-secret',
|
|
)
|
|
builder.assert_called_once_with(
|
|
'postgresql://fixture', bundle_root,
|
|
{'gitlab': build_args.return_value},
|
|
config['supervisor']['worker_api']['compatibility_profiles'],
|
|
'supervisor-fixture', assignment_ttl_seconds=86400,
|
|
assignment_ttl_seconds_by_source={'gitlab': 3660},
|
|
result_upload_body_timeout_seconds=1800,
|
|
credential_refs={'gitlab': 'selected'},
|
|
db_factory=object,
|
|
)
|
|
self.assertEqual(service.db_url, 'postgresql://fixture')
|
|
self.assertIs(service.assignment_builder, builder.return_value)
|
|
self.assertIsNone(service.admin_service)
|
|
self.assertEqual(runtime, {
|
|
'address': '127.0.0.1', 'port': 8766,
|
|
'reaper_interval_seconds': 60, 'limit_concurrency': 64,
|
|
})
|
|
|
|
def test_factory_defensively_rejects_invalid_assignment_ttl_overrides(self):
|
|
for overrides in (
|
|
{'github': 3600},
|
|
{'gitlab': True},
|
|
{'gitlab': 59},
|
|
{'gitlab': 7 * 24 * 60 * 60 + 1},
|
|
):
|
|
with self.subTest(overrides=overrides):
|
|
config = self._config(
|
|
'unused', assignment_ttl_seconds_by_source=overrides,
|
|
)
|
|
with mock.patch.object(
|
|
console_runner, 'load_config', return_value=config,
|
|
):
|
|
with self.assertRaises(ValueError):
|
|
worker_api.build_configured_worker_service(
|
|
'config.yaml', {'instance_id': 'fixture'},
|
|
)
|
|
|
|
with tempfile.TemporaryDirectory() as temp_dir:
|
|
bundle_root = ensure_private_directory(
|
|
os.path.join(temp_dir, 'bundles'), reject_reparse=True,
|
|
)
|
|
config = self._config(
|
|
bundle_root,
|
|
assignment_ttl_seconds_by_source={'gitlab': 3659},
|
|
)
|
|
patches = self._patch_config(config)
|
|
with patches[0], patches[1], patches[2], patches[3], patches[4], \
|
|
patches[5], patches[6]:
|
|
with self.assertRaisesRegex(ValueError, 'for gitlab'):
|
|
worker_api.build_configured_worker_service(
|
|
'config.yaml', {'instance_id': 'fixture'}, db_factory=object,
|
|
)
|
|
|
|
def test_empty_source_list_builds_core_trio_without_direct_credentials(self):
|
|
with tempfile.TemporaryDirectory() as temp_dir:
|
|
bundle_root = ensure_private_directory(
|
|
os.path.join(temp_dir, 'bundles'), reject_reparse=True,
|
|
)
|
|
config = self._config(bundle_root, sources=[])
|
|
config['sources'] = {
|
|
source: {'enabled': True}
|
|
for source in ('gitlab', 'dockerhub', 'huggingface')
|
|
}
|
|
built = {}
|
|
|
|
def build_args(source, *_args, auth_entry=None, **_kwargs):
|
|
value = SimpleNamespace(
|
|
platform={
|
|
'gitlab': 'gitlab', 'dockerhub': 'docker',
|
|
'huggingface': 'huggingface',
|
|
}[source],
|
|
exact_git_planning_enabled=source == 'gitlab', timeout=30,
|
|
token=(auth_entry or {}).get('token', 'ambient-token'),
|
|
docker_username='ambient-user', docker_token='ambient-docker-token',
|
|
auth_name='ambient-auth',
|
|
)
|
|
built[source] = value
|
|
return value
|
|
|
|
with mock.patch.object(console_runner, 'load_config', return_value=config), \
|
|
mock.patch.object(console_runner, 'apply_global_config'), \
|
|
mock.patch.object(console_runner, 'load_secrets', return_value={'fixture': True}), \
|
|
mock.patch.object(console_runner, 'auth_pool_entries', return_value=([{
|
|
'name': 'selected', 'token': 'selected-secret',
|
|
}], None)) as auth_entries, \
|
|
mock.patch.object(console_runner, 'build_args_from_source_config', side_effect=build_args), \
|
|
mock.patch.object(db_backend, 'database_url_from_env', return_value='postgresql://fixture'), \
|
|
mock.patch.object(worker_assignment, 'RemoteAssignmentBuilder') as builder:
|
|
worker_api.build_configured_worker_service(
|
|
'config.yaml', {'instance_id': 'supervisor-fixture'}, db_factory=object,
|
|
)
|
|
|
|
auth_entries.assert_called_once()
|
|
call = builder.call_args
|
|
self.assertEqual(tuple(call.args[2]), ('gitlab', 'dockerhub', 'huggingface'))
|
|
self.assertEqual(call.kwargs['credential_refs'], {
|
|
'gitlab': 'selected', 'dockerhub': '', 'huggingface': '',
|
|
})
|
|
self.assertEqual(built['gitlab'].token, 'selected-secret')
|
|
for source in ('dockerhub', 'huggingface'):
|
|
self.assertEqual(built[source].token, '')
|
|
self.assertEqual(built[source].docker_username, '')
|
|
self.assertEqual(built[source].docker_token, '')
|
|
self.assertIsNone(built[source].auth_name)
|
|
|
|
def test_factory_rejects_direct_auth_entry_before_database_access(self):
|
|
config = self._config(
|
|
'unused', sources=['dockerhub'], auth_entries={'dockerhub': 'private'},
|
|
)
|
|
with mock.patch.object(console_runner, 'load_config', return_value=config), \
|
|
mock.patch.object(db_backend, 'database_url_from_env') as database_url:
|
|
with self.assertRaisesRegex(ValueError, 'only GitHub or GitLab'):
|
|
worker_api.build_configured_worker_service(
|
|
'config.yaml', {'instance_id': 'fixture'},
|
|
)
|
|
database_url.assert_not_called()
|
|
|
|
def test_legacy_github_auth_is_reconciliation_only(self):
|
|
with tempfile.TemporaryDirectory() as temp_dir:
|
|
bundle_root = ensure_private_directory(
|
|
os.path.join(temp_dir, 'bundles'), reject_reparse=True,
|
|
)
|
|
config = self._config(
|
|
bundle_root,
|
|
auth_entries={'gitlab': 'selected', 'github': 'legacy'},
|
|
)
|
|
config['sources']['github'] = {'enabled': True}
|
|
built = {}
|
|
|
|
def build_args(source, *_args, auth_entry=None, **_kwargs):
|
|
value = SimpleNamespace(
|
|
platform=source, exact_git_planning_enabled=True,
|
|
timeout=30, token=(auth_entry or {}).get('token', ''),
|
|
)
|
|
built[source] = value
|
|
return value
|
|
|
|
def auth_entries(_secrets, _source_config):
|
|
return ([
|
|
{'name': 'selected', 'token': 'gitlab-secret'},
|
|
{'name': 'legacy', 'token': 'github-secret'},
|
|
], None)
|
|
|
|
with mock.patch.object(console_runner, 'load_config', return_value=config), \
|
|
mock.patch.object(console_runner, 'apply_global_config'), \
|
|
mock.patch.object(console_runner, 'load_secrets', return_value={'fixture': True}), \
|
|
mock.patch.object(console_runner, 'auth_pool_entries', side_effect=auth_entries), \
|
|
mock.patch.object(console_runner, 'build_args_from_source_config', side_effect=build_args), \
|
|
mock.patch.object(db_backend, 'database_url_from_env', return_value='postgresql://fixture'), \
|
|
mock.patch.object(worker_assignment, 'RemoteAssignmentBuilder') as builder:
|
|
worker_api.build_configured_worker_service(
|
|
'config.yaml', {'instance_id': 'supervisor-fixture'}, db_factory=object,
|
|
)
|
|
|
|
call = builder.call_args
|
|
self.assertEqual(set(call.args[2]), {'gitlab', 'github'})
|
|
self.assertEqual(call.kwargs['credential_refs'], {
|
|
'gitlab': 'selected', 'github': 'legacy',
|
|
})
|
|
self.assertEqual(built['github'].token, 'github-secret')
|
|
self.assertEqual(
|
|
config['supervisor']['worker_api']['sources'], ['gitlab'],
|
|
)
|
|
|
|
def test_factory_builds_admin_only_from_enabled_nested_settings(self):
|
|
with tempfile.TemporaryDirectory() as temp_dir:
|
|
bundle_root = ensure_private_directory(
|
|
os.path.join(temp_dir, 'bundles'), reject_reparse=True,
|
|
)
|
|
config = self._config(bundle_root, admin={
|
|
'enabled': True,
|
|
'origin': 'https://admin.example.test',
|
|
'edge_marker': 'fixture-edge-marker-value-32bytes-minimum',
|
|
'max_body_bytes': 4096,
|
|
'snapshot_limit': 17,
|
|
'requeue_limit': 9,
|
|
'managed_file_roots': {
|
|
'exports': {
|
|
'path': '/data/managed-files/exports',
|
|
'permissions': {
|
|
'list': True, 'read': True,
|
|
'create_replace': True, 'delete': True,
|
|
},
|
|
'limits': {
|
|
'max_relative_path_bytes': 1024,
|
|
'max_component_bytes': 255,
|
|
'max_path_depth': 16,
|
|
'max_listing_entries': 500,
|
|
'max_listing_bytes': 262144,
|
|
'max_file_bytes': 64 * 1024 * 1024,
|
|
},
|
|
},
|
|
},
|
|
})
|
|
patches = self._patch_config(config)
|
|
with patches[0], patches[1], patches[2], patches[3], patches[4], patches[5], \
|
|
patches[6] as builder, mock.patch.object(
|
|
worker_api.HostAgentClient, 'is_available', return_value=True,
|
|
), mock.patch.object(
|
|
worker_api, 'fixed_result_directory_is_safe', return_value=True,
|
|
):
|
|
service, _runtime = worker_api.build_configured_worker_service(
|
|
'config.yaml', {'instance_id': 'supervisor-fixture'},
|
|
db_factory=object,
|
|
)
|
|
|
|
self.assertEqual(service.admin_service.origin, 'https://admin.example.test')
|
|
self.assertEqual(service.admin_service.max_body_bytes, 4096)
|
|
self.assertEqual(service.admin_service.snapshot_limit, 17)
|
|
self.assertEqual(service.admin_service.requeue_limit, 9)
|
|
self.assertEqual(
|
|
service.admin_service.managed_file_roots.root_ids(), ('exports',),
|
|
)
|
|
self.assertEqual(
|
|
service.admin_service.supervisor_metadata,
|
|
{'instance_id': 'supervisor-fixture'},
|
|
)
|
|
self.assertIs(
|
|
service.admin_service.package_compatibility_provider,
|
|
builder.return_value.compatibility_snapshot,
|
|
)
|
|
self.assertIsInstance(
|
|
service.admin_service.runtime_apply_provider.__self__,
|
|
worker_api.HostAgentClient,
|
|
)
|
|
|
|
def test_factory_rejects_admin_unknown_keys(self):
|
|
config = self._config('unused', admin={'enabled': False, 'command': 'restart'})
|
|
with mock.patch.object(console_runner, 'load_config', return_value=config):
|
|
with self.assertRaisesRegex(ValueError, 'unsupported keys'):
|
|
worker_api.build_configured_worker_service(
|
|
'config.yaml', {'instance_id': 'fixture'},
|
|
)
|
|
|
|
def test_factory_rejects_unsafe_managed_root_before_external_access(self):
|
|
config = self._config('unused', admin={
|
|
'enabled': False,
|
|
'managed_file_roots': {
|
|
'private': {
|
|
'path': '/data/config',
|
|
'permissions': {
|
|
'list': True, 'read': True,
|
|
'create_replace': False, 'delete': False,
|
|
},
|
|
'limits': {
|
|
'max_relative_path_bytes': 1024,
|
|
'max_component_bytes': 255,
|
|
'max_path_depth': 16,
|
|
'max_listing_entries': 500,
|
|
'max_listing_bytes': 262144,
|
|
'max_file_bytes': 64 * 1024 * 1024,
|
|
},
|
|
},
|
|
},
|
|
})
|
|
with mock.patch.object(console_runner, 'load_config', return_value=config), \
|
|
mock.patch.object(console_runner, 'load_secrets') as load_secrets, \
|
|
mock.patch.object(db_backend, 'database_url_from_env') as database_url, \
|
|
mock.patch.object(worker_assignment, 'RemoteAssignmentBuilder') as builder:
|
|
with self.assertRaises(managed_files.ManagedFileConfigurationError):
|
|
worker_api.build_configured_worker_service(
|
|
'config.yaml', {'instance_id': 'fixture'},
|
|
)
|
|
load_secrets.assert_not_called()
|
|
database_url.assert_not_called()
|
|
builder.assert_not_called()
|
|
|
|
def test_main_authenticates_before_building_and_never_enables_proxy_headers(self):
|
|
trace = []
|
|
metadata = {'instance_id': 'fixture'}
|
|
runtime = {
|
|
'address': '127.0.0.1', 'port': 8766,
|
|
'reaper_interval_seconds': 60, 'limit_concurrency': 64,
|
|
}
|
|
|
|
def authenticate(*args, **kwargs):
|
|
trace.append('authenticate')
|
|
return metadata
|
|
|
|
def build(*args, **kwargs):
|
|
trace.append('build')
|
|
return object(), runtime
|
|
|
|
with mock.patch.object(worker_api, 'parse_args', return_value=SimpleNamespace(config='config.yaml')), \
|
|
mock.patch.object(lifecycle_authority, 'require_active_supervisor_child', side_effect=authenticate) as authority, \
|
|
mock.patch.object(worker_api, 'build_configured_worker_service', side_effect=build), \
|
|
mock.patch.object(worker_api, 'create_worker_app', return_value='fixture-app'), \
|
|
mock.patch.object(uvicorn, 'run') as run:
|
|
worker_api.main()
|
|
|
|
self.assertEqual(trace, ['authenticate', 'build'])
|
|
authority.assert_called_once_with(
|
|
'config.yaml', child_kind='worker-api', require_dsn=True,
|
|
)
|
|
run.assert_called_once_with(
|
|
'fixture-app', host='127.0.0.1', port=8766,
|
|
access_log=False, proxy_headers=False, server_header=False,
|
|
limit_concurrency=64, timeout_keep_alive=5, workers=1,
|
|
)
|
|
|
|
|
|
class WorkerSupervisorWiringTests(unittest.TestCase):
|
|
def test_child_bootstrap_dispatches_worker_api_from_immutable_authority(self):
|
|
namespace = runpy.run_path(str(APP_DIR / 'child_bootstrap.py'))
|
|
main = namespace['main']
|
|
entrypoint = os.path.normcase(os.path.realpath(APP_DIR / 'worker_api.py'))
|
|
metadata = {'code_manifest': {'files': {'worker-api': {'path': entrypoint}}}}
|
|
enabled = []
|
|
captured = []
|
|
|
|
with mock.patch.object(sys, 'flags', SimpleNamespace(
|
|
isolated=1, no_site=1, dont_write_bytecode=1,
|
|
)), mock.patch.object(sys, 'path', list(sys.path)), mock.patch.object(
|
|
sys, 'argv', [str(APP_DIR / 'child_bootstrap.py'), 'worker-api', '--', '--config', 'config.yaml'],
|
|
), mock.patch.dict(main.__globals__, {
|
|
'_authenticate': lambda kind: (str(APP_DIR), metadata),
|
|
'_enable_dependency_paths': lambda kind: enabled.append(kind),
|
|
}), mock.patch.object(
|
|
main.__globals__['runpy'], 'run_path',
|
|
side_effect=lambda path, run_name=None: captured.append((path, list(sys.argv), run_name)),
|
|
):
|
|
main()
|
|
|
|
self.assertEqual(enabled, ['worker-api'])
|
|
self.assertEqual(captured, [(
|
|
entrypoint, [entrypoint, '--config', 'config.yaml'], '__main__',
|
|
)])
|
|
|
|
def _worker(self, enabled):
|
|
dependency = mock.Mock()
|
|
with mock.patch.object(supervisor, 'require_private_directory', side_effect=lambda path, create=False: path):
|
|
worker = supervisor.ManagedPipelineWorker(
|
|
'worker-api', 'config.yaml', 'project', 'results', {},
|
|
{'enabled': enabled} if enabled is not None else {},
|
|
dependency_gate=dependency,
|
|
child_environment={'TRUF_SUPERVISOR_CHILD_KIND': 'worker-api'},
|
|
)
|
|
return worker, dependency
|
|
|
|
def test_worker_api_child_defaults_disabled_and_uses_authenticated_bootstrap(self):
|
|
disabled, _ = self._worker(None)
|
|
self.assertFalse(disabled.enabled)
|
|
self.assertEqual(disabled.status, 'disabled')
|
|
|
|
enabled, dependency = self._worker(True)
|
|
self.assertTrue(enabled.enabled)
|
|
command = enabled.build_command()
|
|
self.assertEqual(command[:4], [sys.executable, '-I', '-S', '-B'])
|
|
self.assertIn('worker-api', command)
|
|
self.assertEqual(command[-2:], ['--config', enabled.config_path])
|
|
environment = enabled.build_env()
|
|
dependency.force_database_environment.assert_called_once_with(environment)
|
|
self.assertEqual(environment['TRUF_SUPERVISOR_CHILD_KIND'], 'worker-api')
|
|
|
|
|
|
if __name__ == '__main__':
|
|
unittest.main()
|