Initial server source import
This commit is contained in:
@@ -0,0 +1,594 @@
|
||||
"""Isolated entrypoint for the private, read-only Docker deployment."""
|
||||
|
||||
import argparse
|
||||
import http.client
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
import runpy
|
||||
import secrets
|
||||
import signal
|
||||
import stat
|
||||
import subprocess
|
||||
import sys
|
||||
from urllib.parse import quote
|
||||
|
||||
sys.dont_write_bytecode = True
|
||||
if not sys.dont_write_bytecode:
|
||||
raise RuntimeError('container runtime could not disable bytecode writes')
|
||||
|
||||
|
||||
APP = Path('/opt/truf/app')
|
||||
DATA = Path('/data')
|
||||
RUN = Path('/run/truf')
|
||||
UID = GID = 10001
|
||||
DEFAULT_CONFIG = APP / 'config.linux.yaml'
|
||||
PROVISIONED = DATA / '.provisioned.json'
|
||||
INITIALIZED = DATA / 'initialized.json'
|
||||
INITIALIZE_LOCK = DATA / 'initialize.lock'
|
||||
PASSWORD = DATA / 'postgres-password'
|
||||
PROVIDER_SECRETS = DATA / 'config/secrets.yaml'
|
||||
FORMAT = 'truf-container-data-v1'
|
||||
WORKER_HEALTH_TOKEN = '0' * 64
|
||||
DIRECTORIES = (
|
||||
'home', 'config', 'managed-files', 'runtime-linux', 'runtime-linux/results',
|
||||
'runtime-linux/queues', 'runtime-linux/state',
|
||||
'runtime-linux/state/gharchive_cache', 'runtime-linux/logs',
|
||||
'runtime-linux/keychecks', 'runtime-linux/postman_cache',
|
||||
'runtime-linux/result_spool', 'runtime-linux/postgres',
|
||||
'runtime-linux/postgres/logs', 'postgres-linux', 'scanner-work',
|
||||
'scanner-result-bundles', 'scanner-result-bundles/tmp',
|
||||
'scanner-result-bundles/ready', 'scanner-result-bundles/quarantine',
|
||||
)
|
||||
_shutdown_requested = False
|
||||
|
||||
|
||||
def private_path(path, *, directory=False):
|
||||
path = Path(path)
|
||||
if not path.is_absolute() or '..' in path.parts:
|
||||
raise RuntimeError('private path must be absolute and normalized')
|
||||
for component in (*reversed(path.parents), path):
|
||||
if stat.S_ISLNK(component.lstat().st_mode):
|
||||
raise RuntimeError('private paths must not contain symlinks')
|
||||
details = path.lstat()
|
||||
expected_type = stat.S_ISDIR if directory else stat.S_ISREG
|
||||
if (not expected_type(details.st_mode) or details.st_uid != UID
|
||||
or details.st_gid != GID
|
||||
or stat.S_IMODE(details.st_mode) != (0o700 if directory else 0o600)):
|
||||
raise RuntimeError('private path ownership, type, or mode is invalid: ' + str(path))
|
||||
return path
|
||||
|
||||
|
||||
def require_container(*, provisioning=False):
|
||||
if (sys.platform != 'linux' or Path(__file__) != APP / 'container_runtime.py'
|
||||
or not Path('/.dockerenv').is_file()):
|
||||
raise RuntimeError('runtime commands are restricted to the prepared Docker image')
|
||||
if not (sys.flags.isolated and sys.flags.no_site and sys.flags.dont_write_bytecode):
|
||||
raise RuntimeError('container entrypoint requires python -I -S -B')
|
||||
if os.getuid() != os.geteuid() or os.geteuid() != (0 if provisioning else UID):
|
||||
raise RuntimeError('unexpected container runtime UID')
|
||||
if not provisioning and os.getgid() != GID:
|
||||
raise RuntimeError('unexpected container runtime GID')
|
||||
if not os.statvfs(APP).f_flag & os.ST_RDONLY:
|
||||
raise RuntimeError('the application image must be mounted read-only')
|
||||
private_path(APP.parent, directory=True)
|
||||
private_path(APP, directory=True)
|
||||
private_path(APP / 'container_runtime.py')
|
||||
with open('/proc/self/mountinfo', 'rb') as handle:
|
||||
payload = handle.read(1024 * 1024 + 1)
|
||||
if len(payload) > 1024 * 1024:
|
||||
raise RuntimeError('mount inventory exceeds its bound')
|
||||
mounts = {}
|
||||
for line in payload.splitlines():
|
||||
fields = line.split()
|
||||
if len(fields) > 6 and b'-' in fields:
|
||||
mounts[fields[4]] = fields[fields.index(b'-') + 1]
|
||||
if mounts.get(b'/data') not in (b'ext4', b'xfs', b'btrfs', b'zfs'):
|
||||
raise RuntimeError('/data must be an independent native Linux data volume')
|
||||
if mounts.get(b'/run/truf') != b'tmpfs':
|
||||
raise RuntimeError('/run/truf must be an independent private tmpfs')
|
||||
private_path(DATA, directory=True)
|
||||
private_path(RUN, directory=True)
|
||||
os.umask(0o077)
|
||||
|
||||
|
||||
def _read_json(path):
|
||||
path = private_path(path)
|
||||
if path.stat().st_size > 4096:
|
||||
raise RuntimeError('container marker exceeds its bound')
|
||||
value = json.loads(path.read_text(encoding='utf-8'))
|
||||
if not isinstance(value, dict) or value.get('format') != FORMAT:
|
||||
raise RuntimeError('unrecognized container data marker')
|
||||
return value
|
||||
|
||||
|
||||
def _write_new(path, payload, *, provisioning=False):
|
||||
try:
|
||||
private_path(path.parent, directory=True)
|
||||
descriptor = os.open(
|
||||
path,
|
||||
os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW,
|
||||
0o600,
|
||||
)
|
||||
with os.fdopen(descriptor, 'wb') as handle:
|
||||
if provisioning:
|
||||
os.fchown(handle.fileno(), UID, GID)
|
||||
handle.write(payload)
|
||||
handle.flush()
|
||||
os.fsync(handle.fileno())
|
||||
descriptor = os.open(path.parent, os.O_RDONLY | os.O_DIRECTORY)
|
||||
try:
|
||||
os.fsync(descriptor)
|
||||
finally:
|
||||
os.close(descriptor)
|
||||
private_path(path)
|
||||
finally:
|
||||
payload = None
|
||||
|
||||
|
||||
def provision():
|
||||
import fcntl
|
||||
|
||||
lock = DATA / '.provision.lock'
|
||||
try:
|
||||
descriptor = os.open(lock, os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW, 0o600)
|
||||
os.fchown(descriptor, UID, GID)
|
||||
except FileExistsError:
|
||||
private_path(lock)
|
||||
descriptor = os.open(lock, os.O_WRONLY | os.O_NOFOLLOW)
|
||||
with os.fdopen(descriptor, 'wb') as handle:
|
||||
fcntl.flock(handle, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
||||
if PROVISIONED.exists():
|
||||
_read_json(PROVISIONED)
|
||||
managed_files = DATA / 'managed-files'
|
||||
try:
|
||||
os.mkdir(managed_files, 0o700)
|
||||
os.chown(managed_files, UID, GID)
|
||||
except FileExistsError:
|
||||
pass
|
||||
for name in DIRECTORIES:
|
||||
private_path(DATA / name, directory=True)
|
||||
private_path(PASSWORD)
|
||||
private_path(PROVIDER_SECRETS)
|
||||
print('Container data layout is already provisioned; nothing was changed.', flush=True)
|
||||
return
|
||||
if set(os.listdir(DATA)) - {'home', '.provision.lock'}:
|
||||
raise RuntimeError('refusing to provision nonempty or partially initialized data')
|
||||
home = DATA / 'home'
|
||||
if home.exists() and any(home.iterdir()):
|
||||
raise RuntimeError('refusing to provision a nonempty home directory')
|
||||
for name in DIRECTORIES:
|
||||
path = DATA / name
|
||||
if not path.exists():
|
||||
os.mkdir(path, 0o700)
|
||||
os.chown(path, UID, GID)
|
||||
private_path(path, directory=True)
|
||||
_write_new(PASSWORD, (secrets.token_urlsafe(48) + '\n').encode('ascii'), provisioning=True)
|
||||
_write_new(PROVIDER_SECRETS, b'{}\n', provisioning=True)
|
||||
_write_new(DATA / 'runtime-linux/proxy.txt', b'', provisioning=True)
|
||||
_write_new(PROVISIONED, (json.dumps({'format': FORMAT, 'uid': UID, 'gid': GID}) + '\n').encode('ascii'), provisioning=True)
|
||||
print('Fresh private container data layout provisioned; PostgreSQL is not initialized yet.', flush=True)
|
||||
|
||||
|
||||
def prepare_environment(
|
||||
config_path, *, validate_documents=True, return_config_sha256=False,
|
||||
):
|
||||
_read_json(PROVISIONED)
|
||||
for name in ('authority', 'control', 'tmp'):
|
||||
path = RUN / name
|
||||
try:
|
||||
path.mkdir(mode=0o700)
|
||||
except FileExistsError:
|
||||
pass
|
||||
private_path(path, directory=True)
|
||||
config_path = Path(config_path)
|
||||
if config_path != DEFAULT_CONFIG and config_path.parent != DATA / 'config':
|
||||
raise RuntimeError('configuration must be image-owned or in /data/config')
|
||||
private_path(config_path)
|
||||
password = private_path(PASSWORD).read_text(encoding='ascii').rstrip('\n')
|
||||
if re.fullmatch(r'[A-Za-z0-9_-]{32,128}', password) is None:
|
||||
raise RuntimeError('the generated PostgreSQL password is invalid')
|
||||
for name in tuple(os.environ):
|
||||
upper = name.upper()
|
||||
if (upper.startswith(('PG', 'TRUF_', 'SCANNER_', 'SCAN_', 'TRUFFLEHOG_', 'KEYCHECK_'))
|
||||
or upper in ('DATABASE_URL', 'PYTHONPATH', 'PYTHONHOME')):
|
||||
del os.environ[name]
|
||||
url = 'postgresql://truf:' + quote(password, safe='') + '@127.0.0.1:5432/truf'
|
||||
os.environ.update({
|
||||
'PATH': '/usr/local/bin:/usr/bin:/bin:/usr/lib/postgresql/16/bin',
|
||||
'HOME': '/data/home', 'TMPDIR': str(RUN / 'tmp'),
|
||||
'TMP': str(RUN / 'tmp'), 'TEMP': str(RUN / 'tmp'),
|
||||
'TRUF_CONTAINER_CONFIG': str(config_path),
|
||||
'TRUF_POSTGRES_DB': 'truf', 'TRUF_POSTGRES_USER': 'truf',
|
||||
'TRUF_POSTGRES_PORT': '5432', 'TRUF_POSTGRES_PASSWORD': password,
|
||||
'SCANNER_DB_URL': url, 'DATABASE_URL': url, 'TRUF_MANAGED_POSTGRES_DSN': url,
|
||||
'TRUF_DB_CONNECT_TIMEOUT_SEC': '3', 'TRUF_DB_STATEMENT_TIMEOUT_MS': '5000',
|
||||
'TRUF_DB_LOCK_TIMEOUT_MS': '2000', 'TRUF_DB_IDLE_TRANSACTION_TIMEOUT_MS': '10000',
|
||||
})
|
||||
bootstrap = runpy.run_path(str(APP / 'child_bootstrap.py'))
|
||||
bootstrap['_enable_dependency_paths']('supervisor')
|
||||
sys.path.insert(0, str(APP))
|
||||
from paths import apply_path_config
|
||||
from runtime_document_io import (
|
||||
load_managed_runtime_config,
|
||||
validate_managed_runtime_files,
|
||||
)
|
||||
from runtime_security import preflight_lifecycle_paths
|
||||
|
||||
def check_resolved(candidate):
|
||||
expected = {
|
||||
'root_dir': '/opt/truf', 'project_dir': str(APP),
|
||||
'runtime_dir': '/data/runtime-linux', 'postgres_data_dir': '/data/postgres-linux',
|
||||
'postgres_bin_dir': '/usr/lib/postgresql/16/bin',
|
||||
'result_bundle_dir': '/data/scanner-result-bundles', 'work_dir': '/data/scanner-work',
|
||||
'control_dir': str(RUN / 'control'), 'secrets_file': str(PROVIDER_SECRETS),
|
||||
}
|
||||
if any(candidate['global'].get(name) != value for name, value in expected.items()):
|
||||
raise RuntimeError('configuration escapes the fixed container storage contract')
|
||||
if candidate['supervisor'].get('control_dir') != str(RUN / 'control'):
|
||||
raise RuntimeError('supervisor control must remain on private ephemeral storage')
|
||||
preflight_lifecycle_paths(
|
||||
str(config_path), candidate, authority_profile='server',
|
||||
)
|
||||
|
||||
documents = (
|
||||
validate_managed_runtime_files(str(config_path))
|
||||
if validate_documents
|
||||
else load_managed_runtime_config(str(config_path))
|
||||
)
|
||||
config = documents.config
|
||||
config_sha256 = documents.config_sha256
|
||||
documents = None
|
||||
config = apply_path_config(config, str(config_path))
|
||||
check_resolved(config)
|
||||
if return_config_sha256:
|
||||
return config, config_sha256
|
||||
return config
|
||||
|
||||
|
||||
def _bootstrap_command(target, *arguments):
|
||||
return [sys.executable, '-u', '-I', '-S', '-B', str(APP / 'runtime_bootstrap.py'), target, '--', *arguments]
|
||||
|
||||
|
||||
def initialize(config_path, config, *, expected_config_sha256=None):
|
||||
from postgres_runtime import postgres_runtime_paths
|
||||
from runtime_document_io import load_managed_runtime_config
|
||||
from runtime_security import PrivateFileLock, read_private_json, write_private_json_exclusive
|
||||
|
||||
def require_stable_config():
|
||||
if expected_config_sha256 is None:
|
||||
return
|
||||
current = load_managed_runtime_config(str(config_path))
|
||||
current_sha256 = current.config_sha256
|
||||
current = None
|
||||
if current_sha256 != expected_config_sha256:
|
||||
raise RuntimeError('configuration changed after managed runtime validation')
|
||||
|
||||
require_stable_config()
|
||||
paths = postgres_runtime_paths(config)
|
||||
|
||||
def migrate(*, initialize_base):
|
||||
if _shutdown_requested:
|
||||
return False
|
||||
try:
|
||||
# Even an uncertain maintenance start must enter the verified stop path.
|
||||
require_stable_config()
|
||||
subprocess.run(_bootstrap_command(
|
||||
'postgres-runtime', 'maintenance-start', '--config', str(config_path),
|
||||
), check=True)
|
||||
if _shutdown_requested:
|
||||
return False
|
||||
require_stable_config()
|
||||
arguments = [
|
||||
'migrate-runtime-safety', '--config', str(config_path),
|
||||
]
|
||||
if initialize_base:
|
||||
arguments.append('--initialize-base')
|
||||
arguments.extend(('--apply', '--sources-stopped'))
|
||||
subprocess.run(_bootstrap_command(*arguments), check=True)
|
||||
return True
|
||||
finally:
|
||||
subprocess.run(_bootstrap_command(
|
||||
'postgres-runtime', 'maintenance-stop', '--config', str(config_path),
|
||||
), check=True)
|
||||
|
||||
with PrivateFileLock(str(INITIALIZE_LOCK)):
|
||||
if INITIALIZED.exists():
|
||||
marker = _read_json(INITIALIZED)
|
||||
identity = read_private_json(paths['identity_path'])
|
||||
if (not marker.get('system_identifier')
|
||||
or marker.get('system_identifier') != identity.get('system_identifier')
|
||||
or marker.get('pg_major') != 16 or identity.get('pg_major') != 16):
|
||||
raise RuntimeError('initialization marker does not match the bound cluster')
|
||||
migrated = migrate(initialize_base=False)
|
||||
if migrated:
|
||||
print('Existing PostgreSQL migrated and confirmed stopped.', flush=True)
|
||||
return
|
||||
if Path(paths['identity_path']).exists() or any(Path(paths['data_dir']).iterdir()):
|
||||
raise RuntimeError('partial initialization requires offline inspection; no automatic repair is allowed')
|
||||
if _shutdown_requested:
|
||||
return
|
||||
require_stable_config()
|
||||
subprocess.run(_bootstrap_command('postgres-runtime', 'initialize-empty', '--config', str(config_path)), check=True)
|
||||
if _shutdown_requested:
|
||||
return
|
||||
if not migrate(initialize_base=True):
|
||||
return
|
||||
identity = read_private_json(paths['identity_path'])
|
||||
require_stable_config()
|
||||
write_private_json_exclusive(str(INITIALIZED), {
|
||||
'format': FORMAT, 'system_identifier': identity['system_identifier'],
|
||||
'pg_major': identity['pg_major'],
|
||||
})
|
||||
print('Independent PostgreSQL initialized, migrated, cut over, and confirmed stopped.', flush=True)
|
||||
|
||||
|
||||
def _probe_worker_api(config):
|
||||
worker = config['supervisor']['worker_api']
|
||||
connection = None
|
||||
try:
|
||||
connection = http.client.HTTPConnection(
|
||||
worker['address'], int(worker['port']), timeout=2,
|
||||
)
|
||||
connection.request(
|
||||
'POST', '/api/v1/worker/claim', body=b'',
|
||||
headers={
|
||||
'Authorization': 'Bearer ' + WORKER_HEALTH_TOKEN,
|
||||
'Content-Length': '0',
|
||||
},
|
||||
)
|
||||
response = connection.getresponse()
|
||||
payload = response.read(4097)
|
||||
if (
|
||||
len(payload) > 4096
|
||||
or response.status != 401
|
||||
or response.getheader('WWW-Authenticate') != 'Bearer'
|
||||
or json.loads(payload) != {
|
||||
'error': {
|
||||
'code': 'unauthorized',
|
||||
'message': 'worker credentials are invalid',
|
||||
},
|
||||
}
|
||||
):
|
||||
raise RuntimeError('worker API is not ready')
|
||||
except Exception:
|
||||
raise RuntimeError('worker API is not ready') from None
|
||||
finally:
|
||||
if connection is not None:
|
||||
connection.close()
|
||||
|
||||
|
||||
def health(config, *, require_worker_api=False, require_discovery_producers=False):
|
||||
# Never create a competing SQL session while first-install migration is exclusive.
|
||||
_read_json(INITIALIZED)
|
||||
from scanner_db import ScannerDB
|
||||
from lifecycle_authority import DISCOVERY_PRODUCER_SOURCES
|
||||
from supervisor import get_control_snapshot
|
||||
from supervisor_instance import load_instance_metadata
|
||||
|
||||
metadata = load_instance_metadata(config['supervisor']['instance_file'])
|
||||
snapshot = get_control_snapshot(metadata)
|
||||
if (snapshot.get('activation_state') != 'ACTIVE'
|
||||
or snapshot.get('postgres', {}).get('state') != 'READY'
|
||||
or snapshot.get('postgres', {}).get('ready') is not True):
|
||||
raise RuntimeError('supervisor and PostgreSQL are not ready')
|
||||
signatures = {row[0]: row for row in snapshot.get('signature', ())}
|
||||
required = ['result-ingester', 'jsonl-projector']
|
||||
if config['supervisor'].get('janitor', {}).get('enabled', True):
|
||||
required.append('janitor')
|
||||
worker_api_enabled = config['supervisor'].get('worker_api', {}).get(
|
||||
'enabled', False,
|
||||
)
|
||||
if require_worker_api and not worker_api_enabled:
|
||||
raise RuntimeError('worker API is required but disabled')
|
||||
if worker_api_enabled:
|
||||
required.append('worker-api')
|
||||
for name in required:
|
||||
row = signatures.get(name)
|
||||
if not row or tuple(row[1:3]) != ('running', 'running') or not row[3] or row[7]:
|
||||
raise RuntimeError('a required pipeline worker is not running')
|
||||
if any(row[1] == 'failed' or row[8] for row in signatures.values()):
|
||||
raise RuntimeError('a managed source is failed or retains uncertain ownership')
|
||||
if require_discovery_producers:
|
||||
source_config = config.get('sources') or {}
|
||||
supervisor_sources = config['supervisor'].get('sources') or {}
|
||||
enabled_producers = [
|
||||
name for name in DISCOVERY_PRODUCER_SOURCES
|
||||
if (
|
||||
(supervisor_sources.get(name) or {}).get('enabled')
|
||||
if 'enabled' in (supervisor_sources.get(name) or {})
|
||||
else (source_config.get(name) or {}).get('enabled', False)
|
||||
)
|
||||
]
|
||||
for name in enabled_producers:
|
||||
row = signatures.get(name)
|
||||
ready = bool(
|
||||
row
|
||||
and row[2] == 'running'
|
||||
and not row[7]
|
||||
and not row[8]
|
||||
and (
|
||||
(row[1] == 'running' and row[3])
|
||||
or (row[1] == 'waiting' and row[4] == 0)
|
||||
)
|
||||
)
|
||||
if not ready:
|
||||
raise RuntimeError('a required discovery producer is not ready')
|
||||
if require_worker_api:
|
||||
_probe_worker_api(config)
|
||||
db = ScannerDB(db_url=os.environ['SCANNER_DB_URL'], initialize=False)
|
||||
try:
|
||||
if not db.enabled or not db.conn.is_postgres:
|
||||
raise RuntimeError('PostgreSQL application connection is unavailable')
|
||||
db.set_application_name('truf-container-health')
|
||||
db.conn.execute('SET default_transaction_read_only = on')
|
||||
db.conn.commit()
|
||||
db.require_runtime_safety_schema()
|
||||
db.require_final_cutover()
|
||||
for name in ('result_ingester', 'jsonl_projector'):
|
||||
if not db.pipeline_worker_health(name, metadata['instance_id'])['healthy']:
|
||||
raise RuntimeError('a required durable worker lease is not ready')
|
||||
row = db.conn.execute("SELECT current_setting('data_directory') AS data_directory, current_setting('server_version_num') AS version").fetchone()
|
||||
db.conn.commit()
|
||||
if row['data_directory'] != '/data/postgres-linux' or int(row['version']) // 10000 != 16:
|
||||
raise RuntimeError('PostgreSQL identity does not match the container')
|
||||
finally:
|
||||
db.close()
|
||||
for name in ('work_dir', 'result_bundle_dir', 'results_dir'):
|
||||
path = config['global'][name]
|
||||
if not os.access(path, os.W_OK | os.X_OK) or os.statvfs(path).f_bavail == 0:
|
||||
raise RuntimeError('required persistent storage is not writable or is full')
|
||||
return {'healthy': True, 'activation_state': 'ACTIVE', 'postgres': 'READY', 'workers': sorted(signatures)}
|
||||
|
||||
|
||||
def import_secrets(config_path, config, *, expected_config_sha256=None):
|
||||
from postgres_runtime import postgres_runtime_paths
|
||||
from runtime_document_io import (
|
||||
load_managed_runtime_config,
|
||||
validate_managed_runtime_files,
|
||||
)
|
||||
from runtime_security import ClusterAuthorityLock, PrivateFileLock, durable_replace, fsync_directory
|
||||
import yaml
|
||||
|
||||
with PrivateFileLock(str(INITIALIZE_LOCK)), ClusterAuthorityLock(
|
||||
config, create_parent=False, endpoint_dsn=os.environ['SCANNER_DB_URL'],
|
||||
):
|
||||
if (Path(config['supervisor']['instance_file']).exists()
|
||||
or (Path(postgres_runtime_paths(config)['data_dir']) / 'postmaster.pid').exists()):
|
||||
raise RuntimeError('stop the runtime before replacing provider credentials')
|
||||
payload = sys.stdin.buffer.read(1024 * 1024 + 1)
|
||||
try:
|
||||
if not payload or len(payload) > 1024 * 1024:
|
||||
raise RuntimeError('credential input must be a nonempty YAML document below 1 MiB')
|
||||
validated = validate_managed_runtime_files(
|
||||
str(config_path), secrets_bytes=payload,
|
||||
)
|
||||
except BaseException:
|
||||
payload = None
|
||||
raise
|
||||
payload = None
|
||||
validated_config_sha256 = validated.config_sha256
|
||||
if (
|
||||
expected_config_sha256 is not None
|
||||
and validated_config_sha256 != expected_config_sha256
|
||||
):
|
||||
validated = None
|
||||
raise RuntimeError('configuration changed while provider credentials were being validated')
|
||||
try:
|
||||
serialized = yaml.safe_dump(
|
||||
validated.secrets, allow_unicode=True,
|
||||
).encode('utf-8')
|
||||
except BaseException:
|
||||
validated = None
|
||||
raise
|
||||
validated = None
|
||||
temporary = DATA / ('config/secrets-import-' + secrets.token_hex(16))
|
||||
try:
|
||||
try:
|
||||
_write_new(temporary, serialized)
|
||||
finally:
|
||||
serialized = None
|
||||
private_path(PROVIDER_SECRETS)
|
||||
current = load_managed_runtime_config(str(config_path))
|
||||
current_sha256 = current.config_sha256
|
||||
current = None
|
||||
if current_sha256 != validated_config_sha256:
|
||||
raise RuntimeError(
|
||||
'configuration changed while provider credentials were being validated'
|
||||
)
|
||||
durable_replace(str(temporary), str(PROVIDER_SECRETS))
|
||||
fsync_directory(str(PROVIDER_SECRETS.parent))
|
||||
finally:
|
||||
temporary.unlink(missing_ok=True)
|
||||
print('Private provider credentials replaced; no credentials were printed.', flush=True)
|
||||
|
||||
|
||||
def main(argv=None):
|
||||
global _shutdown_requested
|
||||
|
||||
parser = argparse.ArgumentParser(description=__doc__, allow_abbrev=False)
|
||||
parser.add_argument('action', choices=('provision', 'initialize', 'run', 'health', 'status', 'import-secrets', 'import-snapshot'))
|
||||
parser.add_argument('--config', default=os.environ.get('TRUF_CONTAINER_CONFIG', str(DEFAULT_CONFIG)))
|
||||
parser.add_argument('--manifest-sha256')
|
||||
parser.add_argument('--require-worker-api', action='store_true')
|
||||
parser.add_argument('--require-discovery-producers', action='store_true')
|
||||
args = parser.parse_args(argv)
|
||||
require_container(provisioning=args.action == 'provision')
|
||||
if (
|
||||
(args.require_worker_api or args.require_discovery_producers)
|
||||
and args.action not in ('health', 'status')
|
||||
):
|
||||
raise RuntimeError('strict health requirements are restricted to health and status')
|
||||
if args.action == 'import-snapshot':
|
||||
if re.fullmatch(r'[0-9a-f]{64}', args.manifest_sha256 or '') is None:
|
||||
raise RuntimeError('snapshot import requires --manifest-sha256 with the approved manifest digest')
|
||||
if Path(args.config) != DEFAULT_CONFIG:
|
||||
raise RuntimeError('snapshot import must begin with the image-owned default configuration')
|
||||
elif args.manifest_sha256 is not None:
|
||||
raise RuntimeError('--manifest-sha256 is restricted to snapshot import')
|
||||
if args.action == 'provision':
|
||||
provision()
|
||||
return 0
|
||||
bind_config = args.action in ('initialize', 'run', 'import-secrets')
|
||||
prepared = prepare_environment(
|
||||
args.config,
|
||||
validate_documents=args.action != 'import-secrets',
|
||||
return_config_sha256=bind_config,
|
||||
)
|
||||
if bind_config:
|
||||
config, config_sha256 = prepared
|
||||
else:
|
||||
config = prepared
|
||||
if args.action in ('health', 'status'):
|
||||
print(json.dumps(health(
|
||||
config, require_worker_api=args.require_worker_api,
|
||||
require_discovery_producers=args.require_discovery_producers,
|
||||
), sort_keys=True), flush=True)
|
||||
return 0
|
||||
if args.action == 'import-secrets':
|
||||
import_secrets(
|
||||
args.config, config,
|
||||
expected_config_sha256=config_sha256,
|
||||
)
|
||||
return 0
|
||||
|
||||
def request_shutdown(_signum, _frame):
|
||||
global _shutdown_requested
|
||||
_shutdown_requested = True
|
||||
|
||||
previous = signal.signal(signal.SIGTERM, request_shutdown)
|
||||
try:
|
||||
if args.action == 'import-snapshot':
|
||||
from container_import import import_snapshot
|
||||
|
||||
return import_snapshot(sys.modules[__name__], args.manifest_sha256)
|
||||
initialize(
|
||||
Path(args.config), config,
|
||||
expected_config_sha256=config_sha256,
|
||||
)
|
||||
if _shutdown_requested or args.action == 'initialize':
|
||||
return 0
|
||||
from runtime_document_io import load_managed_runtime_config
|
||||
|
||||
current = load_managed_runtime_config(str(args.config))
|
||||
current_sha256 = current.config_sha256
|
||||
current = None
|
||||
if current_sha256 != config_sha256:
|
||||
raise RuntimeError('configuration changed after managed runtime validation')
|
||||
command = _bootstrap_command(
|
||||
'supervisor', '--runtime-bootstrap-entrypoint', str(APP / 'supervisor.py'),
|
||||
'--config', args.config, '--with-postgres', '--non-interactive',
|
||||
'--autostart', '--no-dashboard',
|
||||
)
|
||||
os.execv(sys.executable, command)
|
||||
finally:
|
||||
signal.signal(signal.SIGTERM, previous)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
try:
|
||||
raise SystemExit(main())
|
||||
except Exception as exc:
|
||||
print('Container runtime rejected: ' + str(exc), file=sys.stderr, flush=True)
|
||||
raise SystemExit(1) from None
|
||||
Reference in New Issue
Block a user