Files
truf-server/app/worker_assignment.py
T
2026-09-30 20:30:56 +03:00

1002 lines
44 KiB
Python

import hashlib
import os
import re
from dataclasses import asdict, dataclass
from datetime import datetime, timedelta
from types import MappingProxyType, SimpleNamespace
from console_runner import (
_GitResolutionFailure,
git_resolution_failure_result,
prepare_scan_options,
reserve_v2_admission_with_recovery,
resolve_and_bind_git_claim,
validate_v2_capacity_model,
)
from process_identity import current_process_identity
from result_bundle import BundleReservation, ResultBundleReader, bundle_ready_path
from lifecycle_authority import DISCOVERY_PRODUCER_SOURCES
from scan_execution import (
PROTOCOL_VERSION,
QueueDispositionPolicy,
ScanCompatibility,
ScanExecutionError,
WorkerBuildCompatibility,
canonical_json_sha256,
normalize_docker_direct_execution_snapshot,
normalize_docker_direct_execution_target,
normalize_exact_git_execution_snapshot,
normalize_huggingface_space_execution_snapshot,
normalize_huggingface_space_execution_target,
normalize_remote_scan_policy,
remote_execution_identity,
remote_execution_snapshot_sha256,
stage_scan_result_in_scope,
validate_scan_kwargs,
validate_worker_build_compatibility,
)
from scanner_db import ScannerDB, ScanEventConflictError
from scanner import scan_config
from runtime_security import sha256_file
from worker_contracts import (
DIAGNOSTIC_PROJECTION_VERSION,
ordered_diagnostic_uid_set_sha256,
)
from worker_package import (
KNOWN_WORKER_PACKAGE_CAPABILITIES,
PACKAGE_DETECTOR_POLICY,
load_worker_package_manifest,
normalize_worker_package_manifest,
worker_package_build_compatibility,
)
DEFAULT_ASSIGNMENT_TTL_SECONDS = 24 * 60 * 60
DEFAULT_RESULT_UPLOAD_BODY_TIMEOUT_SECONDS = 30 * 60
PROTOCOL1_NEW_CLAIM_SOURCES = frozenset(('github', 'gitlab'))
SUPPORTED_REMOTE_GIT_SOURCES = PROTOCOL1_NEW_CLAIM_SOURCES
_EXACT_GIT_ASSIGNMENT_FLOW = object()
_DIRECT_ASSIGNMENT_FLOW = object()
NO_WORK_REASONS = frozenset((
'empty_queue', 'assignment_cap', 'dispatch_paused', 'capacity',
'compatibility',
))
_ADMISSION_NO_WORK_REASONS = {
'no_claimable_target': 'empty_queue',
'remote_user_quota_closed': 'assignment_cap',
'remote_global_quota_closed': 'assignment_cap',
'dispatch_gate_closed': 'dispatch_paused',
'pipeline_capacity_closed': 'capacity',
'quarantine_capacity_conflict': 'capacity',
'quarantine_admission_closed': 'capacity',
'ingester_not_ready': 'capacity',
}
_CANONICAL_CAPABILITY_NAME = re.compile(r'[a-z][a-z0-9_]{0,63}\Z')
@dataclass(frozen=True)
class AssignmentCapability:
source: str
platform: str
planning_kind: str
def as_dict(self):
return {
'source': self.source,
'platform': self.platform,
'planning_kind': self.planning_kind,
}
@dataclass(frozen=True)
class AssignmentSourceAdapter:
queue_source: str
worker_platform: str
planning_kind: str
package_capability: AssignmentCapability
snapshot_validator: object
assignment_flow: object = None
def validate_source_args(self, args):
if str(getattr(args, 'platform', '') or '').strip().lower() != self.worker_platform:
raise ValueError('remote assignment source platform does not match its adapter')
if self.assignment_flow is None:
raise ValueError('remote assignment source is not available for new claims')
if self.assignment_flow is _EXACT_GIT_ASSIGNMENT_FLOW and (
self.planning_kind != 'exact_git_v1'
or not bool(getattr(args, 'exact_git_planning_enabled', False))
):
raise ValueError('remote Git sources require enabled exact Git planning')
if self.assignment_flow is _DIRECT_ASSIGNMENT_FLOW:
if self.planning_kind not in {
'docker_direct_v1', 'huggingface_space_v1',
} or any(str(getattr(args, name, '') or '') for name in (
'token', 'docker_username', 'docker_token', 'auth_name',
)):
raise ValueError('remote direct sources must be credential-free')
elif self.assignment_flow is not _EXACT_GIT_ASSIGNMENT_FLOW:
raise ValueError('remote assignment source flow is invalid')
def validate_snapshot(self, value):
normalized = self.snapshot_validator(value)
if (
normalized['execution']['source'] != self.worker_platform
or normalized['planning']['kind'] != self.planning_kind
or normalized['credential_ref']['source'] != self.queue_source
):
raise ScanExecutionError('remote execution snapshot does not match its source adapter')
return normalized
def _exact_git_snapshot(value):
return normalize_exact_git_execution_snapshot(value)
def _adapter(source, platform, planning_kind, *, assignment_flow=None, validator=None):
capability = AssignmentCapability(source, platform, planning_kind)
return AssignmentSourceAdapter(
source, platform, planning_kind, capability,
validator, assignment_flow,
)
LEGACY_GITHUB_ASSIGNMENT_ADAPTER = _adapter(
'github', 'github', 'exact_git_v1',
assignment_flow=_EXACT_GIT_ASSIGNMENT_FLOW,
validator=_exact_git_snapshot,
)
CORE_ASSIGNMENT_SOURCE_ADAPTERS = MappingProxyType({
'gitlab': _adapter(
'gitlab', 'gitlab', 'exact_git_v1',
assignment_flow=_EXACT_GIT_ASSIGNMENT_FLOW,
validator=_exact_git_snapshot,
),
'dockerhub': _adapter(
'dockerhub', 'docker', 'docker_direct_v1',
assignment_flow=_DIRECT_ASSIGNMENT_FLOW,
validator=normalize_docker_direct_execution_snapshot,
),
'huggingface': _adapter(
'huggingface', 'huggingface', 'huggingface_space_v1',
assignment_flow=_DIRECT_ASSIGNMENT_FLOW,
validator=normalize_huggingface_space_execution_snapshot,
),
})
ASSIGNMENT_SOURCE_ADAPTERS = MappingProxyType({
'github': LEGACY_GITHUB_ASSIGNMENT_ADAPTER,
**CORE_ASSIGNMENT_SOURCE_ADAPTERS,
})
PROTOCOL2_NEW_CLAIM_SOURCES = frozenset(CORE_ASSIGNMENT_SOURCE_ADAPTERS)
def _validate_assignment_source_adapters():
if tuple(CORE_ASSIGNMENT_SOURCE_ADAPTERS) != tuple(DISCOVERY_PRODUCER_SOURCES):
raise RuntimeError('core assignment adapters do not match discovery producers')
pairs = set()
for key, adapter in ASSIGNMENT_SOURCE_ADAPTERS.items():
capability = adapter.package_capability
identities = (
key, adapter.queue_source, adapter.worker_platform,
adapter.planning_kind,
)
if any(_CANONICAL_CAPABILITY_NAME.fullmatch(value) is None for value in identities):
raise RuntimeError('remote assignment adapter identity is invalid')
if key != adapter.queue_source or capability.as_dict() != {
'source': adapter.queue_source,
'platform': adapter.worker_platform,
'planning_kind': adapter.planning_kind,
}:
raise RuntimeError('remote assignment adapter capability is inconsistent')
pair = (adapter.queue_source, adapter.worker_platform)
if pair in pairs:
raise RuntimeError('remote assignment adapter source/platform is duplicated')
pairs.add(pair)
if {
source for source, adapter in ASSIGNMENT_SOURCE_ADAPTERS.items()
if adapter.assignment_flow is _EXACT_GIT_ASSIGNMENT_FLOW
} != set(PROTOCOL1_NEW_CLAIM_SOURCES):
raise RuntimeError('protocol-1 assignment source registry is inconsistent')
if {
source for source, adapter in ASSIGNMENT_SOURCE_ADAPTERS.items()
if adapter.assignment_flow is _DIRECT_ASSIGNMENT_FLOW
} != {'dockerhub', 'huggingface'}:
raise RuntimeError('direct assignment source registry is inconsistent')
if {
(
adapter.package_capability.source,
adapter.package_capability.platform,
adapter.package_capability.planning_kind,
)
for adapter in ASSIGNMENT_SOURCE_ADAPTERS.values()
} != set(KNOWN_WORKER_PACKAGE_CAPABILITIES):
raise RuntimeError('worker package capabilities do not match source adapters')
_validate_assignment_source_adapters()
def assignment_source_adapter(source):
source = str(source or '').strip().lower()
adapter = ASSIGNMENT_SOURCE_ADAPTERS.get(source)
if adapter is None:
raise ValueError('unsupported remote assignment source')
return adapter
def _stable_id(device_id, request_id, label):
payload = f'truf-worker-v1\0{int(device_id)}\0{request_id}\0{label}'.encode('ascii')
return hashlib.sha256(payload).hexdigest()[:32]
def _hash_file(path):
digest = hashlib.sha256()
with open(path, 'rb', buffering=0) as handle:
for chunk in iter(lambda: handle.read(1024 * 1024), b''):
digest.update(chunk)
return digest.hexdigest()
class RemoteAssignmentBuilder:
"""Central remote admission builder routed through closed source adapters."""
def __init__(
self, db_url, bundle_root, source_args, compatibility_profiles,
supervisor_instance_id, *, assignment_ttl_seconds=DEFAULT_ASSIGNMENT_TTL_SECONDS,
assignment_ttl_seconds_by_source=None,
result_upload_body_timeout_seconds=DEFAULT_RESULT_UPLOAD_BODY_TIMEOUT_SECONDS,
credential_refs=None,
db_factory=ScannerDB, admission=reserve_v2_admission_with_recovery,
planner=resolve_and_bind_git_claim,
):
self.db_url = str(db_url or '')
self.bundle_root = os.path.abspath(bundle_root)
self.supervisor_instance_id = str(supervisor_instance_id or '')
if not self.db_url or not self.supervisor_instance_id:
raise ValueError('remote assignment builder requires database and supervisor identities')
if (
type(assignment_ttl_seconds) is not int
or not 60 <= assignment_ttl_seconds <= 7 * 24 * 60 * 60
):
raise ValueError('remote assignment lifetime must be between one minute and seven days')
self.assignment_ttl_seconds = assignment_ttl_seconds
raw_ttl_overrides = assignment_ttl_seconds_by_source
if raw_ttl_overrides is None:
raw_ttl_overrides = {}
if (
type(raw_ttl_overrides) is not dict
or set(raw_ttl_overrides) - set(CORE_ASSIGNMENT_SOURCE_ADAPTERS)
):
raise ValueError('remote assignment lifetime overrides contain unsupported sources')
self.assignment_ttl_seconds_by_source = {}
for source, value in raw_ttl_overrides.items():
if type(value) is not int or not 60 <= value <= 7 * 24 * 60 * 60:
raise ValueError('remote assignment lifetime override is outside its bounds')
self.assignment_ttl_seconds_by_source[source] = value
if (
type(result_upload_body_timeout_seconds) is not int
or not 30 <= result_upload_body_timeout_seconds <= 24 * 60 * 60
):
raise ValueError('remote result upload body timeout is outside its bounds')
self.result_upload_body_timeout_seconds = result_upload_body_timeout_seconds
self.db_factory = db_factory
self.admission = admission
self.planner = planner
self.source_args = {}
self.source_adapters = {}
for source, args in dict(source_args or {}).items():
source = str(source or '').strip().lower()
adapter = assignment_source_adapter(source)
adapter.validate_source_args(args)
self.source_args[source] = args
self.source_adapters[source] = adapter
if not self.source_args:
raise ValueError('remote assignment builder has no supported sources')
raw_credential_refs = dict(credential_refs or {})
if set(raw_credential_refs) - set(self.source_args):
raise ValueError('remote credential references contain unsupported sources')
self.credential_refs = {}
for source in self.source_args:
reference = str(raw_credential_refs.get(source) or '')
if len(reference) > 128 or '\x00' in reference:
raise ValueError('remote credential reference is invalid')
if (
self.source_adapters[source].assignment_flow
is _DIRECT_ASSIGNMENT_FLOW and reference
):
raise ValueError('remote direct source credential reference must be empty')
self.credential_refs[source] = reference
self.compatibility_profiles = {}
for profile_name, value in dict(compatibility_profiles or {}).items():
profile_name = str(profile_name or '').strip()
profile = dict(value or {})
if set(profile) - {'package_manifest', 'sources'}:
raise ValueError('remote compatibility profile shape is invalid')
package_value = profile.get('package_manifest')
package = (
load_worker_package_manifest(package_value)
if isinstance(package_value, (str, os.PathLike))
else normalize_worker_package_manifest(package_value)
)
required_build = WorkerBuildCompatibility.from_mapping(
worker_package_build_compatibility(package),
)
package_capabilities = frozenset(
(
item['source'], item['platform'], item['planning_kind'],
)
for item in package['capabilities']
)
package_sources = frozenset(
source for source, _platform, _planning in package_capabilities
)
allowed_sources = frozenset(
str(item or '').strip().lower()
for item in (profile.get('sources') or package_sources)
)
if (
not profile_name or not allowed_sources
or not allowed_sources <= set(self.source_args)
or any(
(
self.source_adapters[source].package_capability.source,
self.source_adapters[source].package_capability.platform,
self.source_adapters[source].package_capability.planning_kind,
) not in package_capabilities
for source in allowed_sources
)
):
raise ValueError('remote compatibility profile has invalid sources')
key = tuple(required_build.as_dict().values())
if key in self.compatibility_profiles:
raise ValueError('remote compatibility profile identity is duplicated')
self.compatibility_profiles[key] = {
'profile_name': profile_name,
'build': required_build,
'package_manifest': package,
'sources': allowed_sources,
'capabilities': package_capabilities,
}
if not self.compatibility_profiles:
raise ValueError('remote assignment builder has no compatibility profiles')
def compatibility_snapshot(self):
profiles = []
for profile in sorted(
self.compatibility_profiles.values(),
key=lambda item: item['profile_name'],
):
profiles.append({
'profile_name': profile['profile_name'],
**profile['build'].as_dict(),
'sources': sorted(profile['sources']),
'capabilities': [
{
'source': source,
'platform': platform,
'planning_kind': planning_kind,
}
for source, platform, planning_kind in sorted(
profile['capabilities'],
)
],
})
required_capabilities = [
self.source_adapters[source].package_capability.as_dict()
for source in sorted(self.source_adapters)
]
return {
'profiles': profiles,
'required_capabilities': required_capabilities,
}
@staticmethod
def _queue_policy(args):
return QueueDispositionPolicy(
target_retry_max_attempts=int(getattr(args, 'target_retry_max_attempts', 3) or 3),
target_retry_base_delay_sec=int(getattr(args, 'target_retry_base_delay_sec', 3600) or 3600),
target_retry_max_delay_sec=int(getattr(args, 'target_retry_max_delay_sec', 86400) or 86400),
target_timeout_retry_delay_sec=int(getattr(args, 'target_timeout_retry_delay_sec', 21600) or 21600),
)
@staticmethod
def _capacity(args):
return {
'bundle_items': int(getattr(args, 'result_bundle_max_items', 10000)),
'bundle_bytes': int(getattr(args, 'result_bundle_max_total_bytes', 3 << 30)),
'projection_items': int(getattr(args, 'projection_backlog_max_items', 10000)),
'projection_bytes': int(getattr(args, 'projection_backlog_max_bytes', 2 << 30)),
'projection_headroom_bytes': int(getattr(
args, 'projection_backlog_headroom_bytes', 0,
)),
'keycheck_items': int(getattr(args, 'keycheck_queue_max_items', 100000)),
'keycheck_bytes': int(getattr(args, 'keycheck_queue_max_bytes', 512 << 20)),
'quarantine_items': int(getattr(args, 'pipeline_quarantine_max_items', 10000)),
'quarantine_bytes': int(getattr(args, 'pipeline_quarantine_max_bytes', 1 << 30)),
}
@staticmethod
def _scan_policy(args):
def setting(name, default):
return getattr(args, name, getattr(scan_config, name, default))
return normalize_remote_scan_policy({
'drop_detectors': setting('drop_detectors', ()),
'strict_git_provider_token_filter': bool(setting(
'strict_git_provider_token_filter', True,
)),
'trufflehog_stdout_max_mb': int(setting('trufflehog_stdout_max_mb', 32)),
'trufflehog_stderr_max_mb': int(setting('trufflehog_stderr_max_mb', 8)),
'result_bundle_max_event_bytes': int(setting(
'result_bundle_max_event_bytes', 64 << 20,
)),
'trufflehog_max_findings_per_target': int(setting(
'trufflehog_max_findings_per_target', 20000,
)),
'trufflehog_job_memory_limit_bytes': int(setting(
'trufflehog_job_memory_limit_bytes', 0,
)),
'trufflehog_windows_job_cpu_weight': int(setting(
'trufflehog_windows_job_cpu_weight', 0,
)),
'trufflehog_windows_memory_priority': int(setting(
'trufflehog_windows_memory_priority', 0,
)),
'trufflehog_diagnostic_max_lines': int(setting(
'trufflehog_diagnostic_max_lines', 2000,
)),
'trufflehog_diagnostic_max_line_chars': int(setting(
'trufflehog_diagnostic_max_line_chars', 8192,
)),
'trufflehog_diagnostic_max_line_bytes': int(setting(
'trufflehog_diagnostic_max_line_bytes', 8192,
)),
'trufflehog_diagnostic_max_errors': int(setting(
'trufflehog_diagnostic_max_errors', 200,
)),
'trufflehog_diagnostic_max_warnings': int(setting(
'trufflehog_diagnostic_max_warnings', 200,
)),
'trufflehog_diagnostic_max_unclassified': int(setting(
'trufflehog_diagnostic_max_unclassified', 20,
)),
})
def _db(self, application_name):
db = self.db_factory(db_url=self.db_url, initialize=False)
if not db.enabled:
db.close()
raise RuntimeError('remote assignment PostgreSQL connection is unavailable')
db.set_application_name(application_name)
return db
def _accept_staged(self, identity, claim, staged):
ready_path = bundle_ready_path(self.bundle_root, claim['bundle_id'])
relative_path = os.path.relpath(ready_path, self.bundle_root).replace(os.sep, '/')
staged_relative_path = str(getattr(staged, 'relative_path', '') or '').replace(
'\\', '/'
)
if staged_relative_path and staged_relative_path != relative_path:
raise RuntimeError('published remote bundle path conflicts with its reservation')
digest = _hash_file(ready_path)
reader = ResultBundleReader(
ready_path, max_event_bytes=int(claim['declared_bundle_bytes']),
)
metadata = reader.validate()
effective_diagnostics = reader.effective_diagnostics()
values = metadata.as_dict()
values['effective_diagnostic_count'] = len(effective_diagnostics)
values['effective_diagnostic_projection_version'] = (
DIAGNOSTIC_PROJECTION_VERSION
)
values['effective_diagnostic_uids_sha256'] = (
ordered_diagnostic_uid_set_sha256(effective_diagnostics)
)
values['relative_path'] = relative_path
db = self._db('truf-worker-planning-result')
try:
return db.mark_result_bundle_ready(
int(claim['reservation_id']), values,
remote_acceptance={
'device_id': int(identity['device_id']),
'payload_sha256': digest,
'token_sha256': str(identity['token_sha256']),
},
bundle_capacity_bytes=self._capacity(
self.source_args[claim['source']]
)['bundle_bytes'],
)
finally:
db.close()
def _recover_published(self, identity, claim):
path = bundle_ready_path(self.bundle_root, claim['bundle_id'])
if not os.path.lexists(path):
return False
metadata = ResultBundleReader(
path, max_event_bytes=int(claim['declared_bundle_bytes']),
).validate()
if metadata.header != BundleReservation.from_mapping(claim).header():
raise RuntimeError('published remote bundle conflicts with its reservation')
self._accept_staged(identity, claim, metadata)
return True
def _bound_plan(self, identity, claim):
db = self._db('truf-worker-plan-reconcile')
try:
return db.remote_bound_git_scan_plan(
int(claim['reservation_id']), int(identity['device_id']),
str(claim['claim_lease_token']), str(identity['token_sha256']),
)
finally:
db.close()
def _reconcile_request(self, identity, request_id):
db = self._db('truf-worker-request-reconcile')
try:
return db.reconcile_remote_assignment_request(
request_id, int(identity['device_id']), str(identity['token_sha256']),
)
finally:
db.close()
def _snapshot(self, adapter, args, compatibility, execution):
planning = {'kind': adapter.planning_kind}
if adapter.assignment_flow is _EXACT_GIT_ASSIGNMENT_FLOW:
planning.update({
'git_baseline_depth': max(
1, int(getattr(args, 'git_baseline_depth', 100) or 100),
),
'git_ref_resolution_attempts': max(
1, int(getattr(args, 'git_ref_resolution_attempts', 2) or 2),
),
'git_ref_resolution_timeout_sec': max(
0.1, float(getattr(args, 'git_ref_resolution_timeout_sec', 10) or 10),
),
'git_ref_resolution_max_bytes': max(
1024, int(getattr(args, 'git_ref_resolution_max_bytes', 1 << 20) or (1 << 20)),
),
})
return adapter.validate_snapshot({
'schema': 1,
'compatibility': compatibility.as_dict(),
'execution': execution,
'planning': planning,
'credential_ref': {
'source': adapter.queue_source,
'auth_entry': self.credential_refs[adapter.queue_source],
},
})
def _rehydrate(self, client, claim, snapshot, snapshot_sha256=None):
adapter = assignment_source_adapter(claim.get('source'))
if (
self.source_adapters.get(adapter.queue_source) is not adapter
or str(claim.get('platform') or '') != adapter.worker_platform
or adapter.assignment_flow not in {
_EXACT_GIT_ASSIGNMENT_FLOW, _DIRECT_ASSIGNMENT_FLOW,
}
):
raise RuntimeError('remote assignment source adapter is unavailable')
snapshot = adapter.validate_snapshot(snapshot)
if (
snapshot_sha256 is not None
and str(snapshot_sha256) != remote_execution_snapshot_sha256(snapshot)
):
raise ScanEventConflictError('remote execution snapshot changed after admission')
required = ScanCompatibility.from_mapping(snapshot['compatibility'])
required_build = WorkerBuildCompatibility.from_mapping({
'protocol_version': required.protocol_version,
'bundle_format_version': required.bundle_format_version,
'platform_tag': required.platform_tag,
'code_manifest_sha256': required.code_manifest_sha256,
'detector_policy_sha256': required.detector_policy_sha256,
})
if required.protocol_version not in {1, PROTOCOL_VERSION}:
raise ScanEventConflictError(
'remote execution snapshot protocol is unsupported'
)
validate_worker_build_compatibility(
required_build, client,
expected_protocol_version=required.protocol_version,
)
source = adapter.queue_source
args = self.source_args.get(source)
if args is None or snapshot['credential_ref'] != {
'source': source, 'auth_entry': self.credential_refs.get(source),
}:
raise RuntimeError('remote assignment credential reference is unavailable')
values = vars(args).copy()
values.update(snapshot['planning'])
values.pop('kind', None)
planning_args = SimpleNamespace(**values)
event_scan_options = dict(snapshot['execution']['scan_kwargs'])
scan_kwargs = dict(event_scan_options)
if adapter.assignment_flow is _EXACT_GIT_ASSIGNMENT_FLOW:
scan_kwargs['token'] = str(getattr(args, 'token', '') or '')
return (
adapter, planning_args, snapshot, scan_kwargs, event_scan_options,
QueueDispositionPolicy(**snapshot['execution']['queue_policy']),
dict(snapshot['execution']['limits']),
dict(snapshot['execution']['scan_policy']), required,
)
def _complete_claim(
self, identity, client, claim, snapshot, *, snapshot_sha256=None,
recovered_execution_plan=None,
):
(
adapter, args, snapshot, scan_kwargs, event_scan_options, queue_policy,
limits, scan_policy, required,
) = self._rehydrate(client, claim, snapshot, snapshot_sha256)
device_id = int(identity['device_id'])
status_db = self._db('truf-worker-claim-reconcile')
try:
status = status_db.remote_assignment_status(
int(claim['reservation_id']), device_id,
str(identity['token_sha256']),
)
finally:
status_db.close()
if status and status.get('receipt_id'):
return None
if self._recover_published(identity, claim):
return None
if adapter.assignment_flow is _EXACT_GIT_ASSIGNMENT_FLOW:
execution_target = str(claim.get('target') or '')
elif adapter.planning_kind == 'docker_direct_v1':
execution_target = normalize_docker_direct_execution_target(
claim.get('target'),
)['image']
elif adapter.planning_kind == 'huggingface_space_v1':
execution_target = normalize_huggingface_space_execution_target(
claim.get('target'),
)
else:
raise RuntimeError('remote assignment execution plan is unavailable')
bound_plan = None
if recovered_execution_plan is not None:
if (
not isinstance(recovered_execution_plan, dict)
or set(recovered_execution_plan) != {
'kind', 'execution_target', 'bound_plan',
}
or recovered_execution_plan['kind'] != adapter.planning_kind
or str(recovered_execution_plan['execution_target']) != execution_target
or (
adapter.assignment_flow is _EXACT_GIT_ASSIGNMENT_FLOW
and not isinstance(recovered_execution_plan['bound_plan'], dict)
)
or (
adapter.assignment_flow is _DIRECT_ASSIGNMENT_FLOW
and recovered_execution_plan['bound_plan'] is not None
)
):
raise ScanEventConflictError(
'recovered execution plan conflicts with its assignment'
)
bound_plan = recovered_execution_plan['bound_plan']
if (
adapter.assignment_flow is _EXACT_GIT_ASSIGNMENT_FLOW
and bound_plan is None
):
bound_plan = self._bound_plan(identity, claim)
if (
adapter.assignment_flow is _EXACT_GIT_ASSIGNMENT_FLOW
and bound_plan is None
):
try:
bound_plan = self.planner(
args, self.db_url, adapter.queue_source, claim, scan_kwargs,
remote_credential={
'device_id': device_id,
'token_sha256': str(identity['token_sha256']),
},
)
except ScanEventConflictError:
bound_plan = self._bound_plan(identity, claim)
if bound_plan is None:
raise
except _GitResolutionFailure as failure:
bound_plan = self._bound_plan(identity, claim)
if bound_plan is None:
result = git_resolution_failure_result(args, claim, scan_kwargs, failure)
staged = stage_scan_result_in_scope(
result, claim, self.bundle_root, event_scan_options, queue_policy,
attempts=int(claim.get('attempts') or 0),
candidate_max_items=limits['candidate_max_items'],
candidate_max_bytes=limits['candidate_max_bytes'],
require_s_drive=False,
)
self._accept_staged(identity, claim, staged)
return None
assigned_scan_kwargs = dict(scan_kwargs)
if adapter.assignment_flow is _EXACT_GIT_ASSIGNMENT_FLOW:
assigned_scan_kwargs['git_plan'] = bound_plan
execution_plan = {
'kind': adapter.planning_kind,
'execution_target': execution_target,
'bound_plan': bound_plan,
}
issued_at = str(claim.get('remote_issued_at') or '')
deadline_at = str(claim.get('remote_expires_at') or '')
try:
issued = datetime.fromisoformat(issued_at)
deadline = datetime.fromisoformat(deadline_at)
except ValueError as exc:
raise RuntimeError('remote assignment reservation deadline is invalid') from exc
if (
issued.tzinfo is None
or deadline.tzinfo is None
or issued.utcoffset() != timedelta(0)
or deadline.utcoffset() != timedelta(0)
or issued.isoformat(timespec='seconds') != issued_at
or deadline.isoformat(timespec='seconds') != deadline_at
):
raise RuntimeError('remote assignment reservation deadline is invalid')
assignment_ttl = (deadline - issued).total_seconds()
if (
not assignment_ttl.is_integer()
or not 60 <= assignment_ttl <= 7 * 24 * 60 * 60
):
raise RuntimeError('remote assignment reservation lifetime is invalid')
target_scan_timeout = scan_kwargs.get('timeout_sec')
if (
isinstance(target_scan_timeout, bool)
or not isinstance(target_scan_timeout, (int, float))
or not float(target_scan_timeout).is_integer()
or target_scan_timeout <= 0
):
raise RuntimeError('remote assignment target scan timeout is invalid')
persisted_upload_timeout = claim.get(
'remote_result_upload_body_timeout_seconds'
)
if persisted_upload_timeout is None:
persisted_upload_timeout = self.result_upload_body_timeout_seconds
if (
isinstance(persisted_upload_timeout, bool)
or not isinstance(persisted_upload_timeout, int)
or not 30 <= persisted_upload_timeout <= 24 * 60 * 60
):
raise RuntimeError('remote assignment result upload timeout is invalid')
reservation = dict(claim)
reservation.pop('remote_result_upload_body_timeout_seconds', None)
return {
'reservation': reservation,
'deadlines': {
'target_scan_timeout_seconds': int(target_scan_timeout),
'result_upload_body_timeout_seconds': persisted_upload_timeout,
'assignment_ttl_seconds': int(assignment_ttl),
'assignment_issued_at': issued_at,
'assignment_deadline_at': deadline_at,
},
'compatibility': required.as_dict(),
'scan_kwargs': assigned_scan_kwargs,
'event_scan_options': event_scan_options,
'queue_policy': asdict(queue_policy),
'limits': limits,
'scan_policy': scan_policy,
'execution_snapshot': snapshot,
'execution_snapshot_sha256': remote_execution_snapshot_sha256(snapshot),
'execution_plan': execution_plan,
}
def __call__(self, identity, request_id, client_compatibility):
client = WorkerBuildCompatibility.from_mapping(client_compatibility)
reconciled = self._reconcile_request(identity, request_id)
if reconciled is not None:
if reconciled.get('state') != 'committed':
return None
if reconciled.get('receipt'):
return {'resolution': dict(reconciled['receipt'])}
execution_plan = reconciled.get('execution_plan')
if execution_plan is None:
if assignment_source_adapter(
reconciled['claim'].get('source')
).assignment_flow is not _EXACT_GIT_ASSIGNMENT_FLOW:
raise ScanEventConflictError(
'direct assignment execution plan is unavailable'
)
execution_plan = {
'kind': 'exact_git_v1',
'execution_target': reconciled['claim']['target'],
'bound_plan': reconciled.get('git_plan'),
}
return self._complete_claim(
identity, client, reconciled['claim'],
reconciled['execution_snapshot'],
snapshot_sha256=reconciled['execution_snapshot_sha256'],
recovered_execution_plan=execution_plan,
)
profile = self.compatibility_profiles.get(tuple(client.as_dict().values()))
if profile is None:
return {'no_assignment': {'reason': 'compatibility'}}
required_build = validate_worker_build_compatibility(profile['build'], client)
candidates = []
for source in sorted(profile['sources']):
adapter = self.source_adapters[source]
capability = adapter.package_capability
if (
capability.source, capability.platform, capability.planning_kind,
) not in profile['capabilities']:
raise RuntimeError(
'remote package capability changed after profile validation'
)
values = vars(self.source_args[source]).copy()
central_policy = str(values.get('trufflehog_config') or '')
if (
not central_policy or not os.path.isfile(central_policy)
or sha256_file(central_policy)
!= required_build.detector_policy_sha256
):
raise RuntimeError('remote package detector policy does not match central authority')
values['trufflehog_config'] = PACKAGE_DETECTOR_POLICY
source_args = SimpleNamespace(**values)
source_scan_kwargs = validate_scan_kwargs(
adapter.worker_platform,
prepare_scan_options(source_args, 1, quiet=True),
)
source_event_options = {
key: value for key, value in source_scan_kwargs.items() if key != 'token'
}
source_queue_policy = self._queue_policy(source_args)
source_limits = {
'candidate_max_items': int(getattr(source_args, 'keycheck_candidates_per_event', 2000)),
'candidate_max_bytes': int(getattr(source_args, 'keycheck_candidate_bytes_per_event', 2 << 20)),
}
source_scan_policy = self._scan_policy(source_args)
effective_identity, execution = remote_execution_identity(
adapter.worker_platform, source_scan_kwargs, source_event_options,
source_queue_policy, source_limits, source_scan_policy,
)
required = ScanCompatibility(
protocol_version=required_build.protocol_version,
bundle_format_version=required_build.bundle_format_version,
platform_tag=required_build.platform_tag,
code_manifest_sha256=required_build.code_manifest_sha256,
effective_config_sha256=effective_identity,
detector_policy_sha256=required_build.detector_policy_sha256,
)
snapshot = self._snapshot(adapter, source_args, required, execution)
candidates.append((
adapter, source_args, source_scan_kwargs, source_event_options,
source_queue_policy, source_limits, source_scan_policy, required,
snapshot,
))
if not candidates:
raise RuntimeError('remote compatibility profile has no central source configuration')
device_id = int(identity['device_id'])
producer = current_process_identity().as_dict()
start = int(request_id[:16], 16) % len(candidates)
ordered_candidates = candidates[start:] + candidates[:start]
multiple_sources = len(ordered_candidates) > 1
no_work_reasons = []
for candidate in ordered_candidates:
(
adapter, args, scan_kwargs, event_scan_options, queue_policy, limits,
scan_policy, required, snapshot,
) = candidate
admission_token = (
_stable_id(
device_id, request_id,
f'admission:{adapter.queue_source}',
)
if multiple_sources else request_id
)
if multiple_sources:
recovered = self._reconcile_request(identity, admission_token)
if recovered is not None:
if recovered.get('state') != 'committed':
continue
if recovered.get('receipt'):
return {'resolution': dict(recovered['receipt'])}
execution_plan = recovered.get('execution_plan')
if execution_plan is None:
if assignment_source_adapter(
recovered['claim'].get('source')
).assignment_flow is not _EXACT_GIT_ASSIGNMENT_FLOW:
raise ScanEventConflictError(
'direct assignment execution plan is unavailable'
)
execution_plan = {
'kind': 'exact_git_v1',
'execution_target': recovered['claim']['target'],
'bound_plan': recovered.get('git_plan'),
}
return self._complete_claim(
identity, client, recovered['claim'],
recovered['execution_snapshot'],
snapshot_sha256=recovered[
'execution_snapshot_sha256'
],
recovered_execution_plan=execution_plan,
)
max_event_bytes = int(getattr(
args, 'result_bundle_max_event_bytes', 64 << 20,
))
remote_reserve_bytes = int(getattr(
args, 'remote_assignment_reserve_bytes', 2 << 20,
))
remote_max_active = int(getattr(
args, 'remote_assignment_max_active', 50,
))
validate_v2_capacity_model(
int(getattr(args, 'max_active_scans', 1) or 1),
max_event_bytes,
int(getattr(args, 'projection_backlog_max_bytes', 2 << 30)),
int(getattr(
args, 'projection_backlog_headroom_bytes',
max_event_bytes * 2,
)),
)
outcome = self.admission(
self.db_url, adapter.queue_source, adapter.worker_platform,
producer, self.supervisor_instance_id,
max_event_bytes, remote_reserve_bytes,
limits['candidate_max_items'], limits['candidate_max_bytes'],
lease_seconds=self.assignment_ttl_seconds_by_source.get(
adapter.queue_source, self.assignment_ttl_seconds,
),
max_attempts=int(getattr(
args, 'target_retry_max_attempts', 3,
) or 3),
capacity_limits=self._capacity(args), run_id=None, cycle_id=None,
reservation_token=admission_token,
bundle_id=_stable_id(device_id, admission_token, 'bundle'),
scan_event_id=_stable_id(device_id, admission_token, 'event'),
resolution_attempts=int(getattr(
args, 'admission_resolution_attempts', 8,
) or 8),
resolution_seconds=float(getattr(
args, 'admission_resolution_seconds', 30,
) or 30),
retry_delay=float(getattr(
args, 'admission_resolution_retry_delay_sec', 0.2,
) or 0.2),
claim_order=str(getattr(
args, 'target_claim_order', 'oldest',
) or 'oldest'),
final_cutover=True,
reserved_bundle_bytes=remote_reserve_bytes,
remote_max_active=remote_max_active,
db_factory=self.db_factory,
remote_assignment={
'user_id': int(identity['user_id']),
'device_id': device_id,
'effective_config_sha256': required.effective_config_sha256,
'client_compat_sha256': canonical_json_sha256(
client.as_dict()
),
'token_sha256': str(identity['token_sha256']),
'result_upload_body_timeout_seconds': (
self.result_upload_body_timeout_seconds
),
'execution_snapshot': snapshot,
},
)
if outcome.claim is not None:
return self._complete_claim(
identity, client, outcome.claim, snapshot,
)
reason = _ADMISSION_NO_WORK_REASONS.get(str(
getattr(outcome, 'reason', None) or ''
))
if reason:
no_work_reasons.append(reason)
if no_work_reasons:
priority = (
'dispatch_paused', 'assignment_cap', 'capacity', 'empty_queue',
)
return {'no_assignment': {'reason': next(
reason for reason in priority if reason in no_work_reasons
)}}
return None
RemoteGitAssignmentBuilder = RemoteAssignmentBuilder