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

292 lines
14 KiB
Python

import importlib.util
import json
import os
from pathlib import Path
import stat
import sys
import tempfile
import unittest
from unittest import mock
ROOT = Path(__file__).resolve().parents[1]
DEPLOY = ROOT / 'deploy' / 'host-agent'
INSTALLER_PATH = DEPLOY / 'truf_host_agent_install.py'
SPEC = importlib.util.spec_from_file_location('truf_host_agent_install', INSTALLER_PATH)
installer = importlib.util.module_from_spec(SPEC)
SPEC.loader.exec_module(installer)
class HostAgentDeployTests(unittest.TestCase):
def test_secure_executable_accepts_root_owned_usr_bin_symlink(self):
link = mock.Mock(st_uid=0, st_mode=stat.S_IFLNK | 0o777)
target = mock.Mock(st_uid=0, st_mode=stat.S_IFREG | 0o755)
parent = mock.Mock(st_uid=0, st_mode=stat.S_IFDIR | 0o755)
with mock.patch.object(installer.os, 'lstat', return_value=link), \
mock.patch.object(installer.os.path, 'realpath', return_value='/usr/bin/python3.14'), \
mock.patch.object(installer.os, 'stat', side_effect=(target, parent)):
installer._secure_executable('/usr/bin/python3')
def test_secure_executable_rejects_symlink_outside_usr_bin(self):
link = mock.Mock(st_uid=0, st_mode=stat.S_IFLNK | 0o777)
target = mock.Mock(st_uid=0, st_mode=stat.S_IFREG | 0o755)
parent = mock.Mock(st_uid=0, st_mode=stat.S_IFDIR | 0o755)
with mock.patch.object(installer.os, 'lstat', return_value=link), \
mock.patch.object(installer.os.path, 'realpath', return_value='/tmp/python3'), \
mock.patch.object(installer.os, 'stat', side_effect=(target, parent)), \
self.assertRaises(installer.InstallError):
installer._secure_executable('/usr/bin/python3')
def test_socket_unit_exposes_only_fixed_runtime_socket(self):
unit = (DEPLOY / 'truf-host-agent.socket').read_text(encoding='ascii')
self.assertIn('ListenStream=/run/truf/host-agent.sock\n', unit)
self.assertIn('SocketUser=root\n', unit)
self.assertIn('SocketGroup=truf-runtime\n', unit)
self.assertIn('SocketMode=0660\n', unit)
self.assertIn('Accept=no\n', unit)
self.assertNotIn('%', unit)
self.assertNotIn('Environment', unit)
def test_service_has_fixed_entrypoints_and_narrow_host_authority(self):
unit = (DEPLOY / 'truf-host-agent.service').read_text(encoding='ascii')
self.assertIn(
'ExecStartPre=/usr/bin/python3 -I -B '
'/usr/lib/truf-host-agent/truf_host_agent_install.py validate\n',
unit,
)
self.assertIn(
'ExecStart=/usr/bin/python3 -I -B '
'/usr/lib/truf-host-agent/truf_host_agent.py\n',
unit,
)
for directive in (
'User=root', 'NoNewPrivileges=yes', 'PrivateNetwork=yes',
'ProtectSystem=strict', 'RestrictAddressFamilies=AF_UNIX',
'ReadWritePaths=/etc/truf/runtime',
'ReadWritePaths=/var/lib/truf/host-agent',
'ReadWritePaths=/run/truf-host-agent',
'ReadWritePaths=/run/docker.sock',
'ReadOnlyPaths=/var/lib/truf/runtime-document-candidates',
'Restart=no',
):
self.assertIn(directive + '\n', unit)
self.assertNotIn('Environment=', unit)
self.assertNotIn('/bin/sh', unit)
self.assertIn('StandardOutput=null\n', unit)
self.assertIn('StandardError=journal\n', unit)
def test_tmpfiles_declares_only_fixed_directories_and_modes(self):
lines = (DEPLOY / 'truf-host-agent.conf').read_text(
encoding='ascii',
).splitlines()
self.assertEqual(lines, [
'd /etc/truf/runtime 0755 root root -',
'd /etc/truf/worker-packages 0755 root root -',
'd /var/lib/truf/runtime-document-candidates 0700 10001 10001 -',
'd /var/lib/truf/host-agent 0700 root root -',
'd /var/lib/truf/host-agent/backups 0700 root root -',
'd /var/lib/truf/host-agent/operations 0700 root root -',
'd /var/lib/truf/host-agent/results 0750 root 10001 -',
'd /run/truf-postgres 0700 10001 10001 -',
])
class HostAgentInstallerTests(unittest.TestCase):
def test_profile_is_exact_root_owned_policy_with_standalone_default(self):
with tempfile.TemporaryDirectory() as temporary:
missing = Path(temporary) / 'missing'
with mock.patch.object(installer, 'DEPLOYMENT_PROFILE', missing):
self.assertIs(installer._deployment_profile(), installer.STANDALONE_PROFILE)
selected = Path(temporary) / 'profile'
selected.write_text('shared-host-edge-v1\n', encoding='ascii')
selected.chmod(0o444)
with mock.patch.object(installer, 'DEPLOYMENT_PROFILE', selected):
self.assertIs(installer._deployment_profile(), installer.SHARED_HOST_PROFILE)
selected.chmod(0o666)
with mock.patch.object(installer, 'DEPLOYMENT_PROFILE', selected), \
self.assertRaises(installer.InstallError):
installer._deployment_profile()
def test_main_accepts_only_fixed_install_or_validate_action(self):
with mock.patch.object(sys, 'argv', ['installer', 'validate']), \
mock.patch.object(installer, 'validate') as validate:
self.assertEqual(installer.main(), 0)
validate.assert_called_once_with()
with mock.patch.object(sys, 'argv', ['installer', 'install']), \
mock.patch.object(installer, 'install') as install:
self.assertEqual(installer.main(), 0)
install.assert_called_once_with()
for arguments in ([], ['restart'], ['validate', 'extra']):
with self.subTest(arguments=arguments), \
mock.patch.object(sys, 'argv', ['installer', *arguments]):
with self.assertRaises(installer.InstallError):
installer.main()
def test_install_uses_fixed_sources_and_never_replaces_active_documents(self):
reads = []
def read_source(path, maximum=installer.MAX_COPY_BYTES):
reads.append((path, maximum))
return str(path).encode('ascii')
with mock.patch.object(installer.sys, 'platform', 'linux'), \
mock.patch.object(installer.os, 'geteuid', return_value=0, create=True), \
mock.patch.object(installer, '_ensure_runtime_group'), \
mock.patch.object(installer, '_unit_active', return_value=True) as active, \
mock.patch.object(installer, '_ensure_install_root'), \
mock.patch.object(installer, '_read_source', side_effect=read_source), \
mock.patch.object(installer, '_write') as write, \
mock.patch.object(installer, '_run') as run, \
mock.patch.object(installer, 'validate') as validate, \
mock.patch.object(installer, '_validate_agent_socket') as socket:
installer.install()
self.assertEqual([call.args[0] for call in write.call_args_list], [
installer.INSTALL_ROOT / 'truf_host_agent.py',
installer.INSTALL_ROOT / 'truf_host_agent_install.py',
installer.SYSTEMD / 'truf-host-agent.socket',
installer.SYSTEMD / 'truf-host-agent.service',
installer.TMPFILES,
installer.ACTIVE / 'config.yaml',
installer.ACTIVE / 'secrets.yaml',
])
self.assertTrue(all(call.kwargs['replace'] for call in write.call_args_list[:5]))
self.assertTrue(all(not call.kwargs['replace'] for call in write.call_args_list[5:]))
self.assertEqual(reads, [
(installer.DEPLOY / 'truf_host_agent.py', installer.MAX_COPY_BYTES),
(installer.DEPLOY / 'truf_host_agent_install.py', installer.MAX_COPY_BYTES),
(installer.DEPLOY / 'truf-host-agent.socket', installer.MAX_COPY_BYTES),
(installer.DEPLOY / 'truf-host-agent.service', installer.MAX_COPY_BYTES),
(installer.DEPLOY / 'truf-host-agent.conf', installer.MAX_COPY_BYTES),
(installer.PROJECT / 'app/config.linux.yaml', installer.MAX_COPY_BYTES),
])
self.assertEqual([call.args[0] for call in run.call_args_list], [
('/usr/bin/systemctl', 'stop', 'truf-host-agent.socket'),
('/usr/bin/systemctl', 'stop', 'truf-host-agent.service'),
('/usr/bin/systemd-tmpfiles', '--create', str(installer.TMPFILES)),
('/usr/bin/systemctl', 'daemon-reload'),
('/usr/bin/systemctl', 'enable', 'truf-host-agent.socket'),
('/usr/bin/systemctl', 'restart', 'truf-host-agent.socket'),
])
self.assertEqual(
[call.args[0] for call in active.call_args_list],
list(installer.UNITS),
)
validate.assert_called_once_with(require_socket=False)
socket.assert_called_once_with()
def test_compose_projection_requires_exact_runtime_mounts(self):
mounts = []
for kind, source, target, read_only, create_host_path in installer.EXPECTED_RUNTIME_MOUNTS:
mount = {'type': kind, 'source': source, 'target': target}
if read_only:
mount['read_only'] = True
if create_host_path is not None:
mount['bind'] = {'create_host_path': create_host_path}
mounts.append(mount)
projection = {
'name': 'truf-docker',
'volumes': {'data': {'name': 'truf-docker_data'}},
'services': {
'runtime': {
'network_mode': None,
'ports': installer.STANDALONE_PROFILE['runtime_ports'],
'cpus': 2.0,
'mem_limit': str(6 * 1024 ** 3),
'volumes': mounts,
},
'edge': {
'network_mode': 'service:runtime',
'cap_add': ['NET_BIND_SERVICE'],
'image': 'truf-local:edge',
},
},
}
installer._validate_compose_projection(json.dumps(projection).encode('ascii'))
normalized = json.loads(json.dumps(projection))
for mount in normalized['services']['runtime']['volumes']:
if mount['type'] == 'bind':
mount['bind'] = {}
installer._validate_compose_projection(json.dumps(normalized).encode('ascii'))
mounts[1]['source'] = '/etc/truf/substituted'
with self.assertRaises(installer.InstallError):
installer._validate_compose_projection(json.dumps(projection).encode('ascii'))
def test_compose_projection_accepts_only_exact_shared_host_profile(self):
mounts = []
for kind, source, target, read_only, create_host_path in installer.EXPECTED_RUNTIME_MOUNTS:
mount = {'type': kind, 'source': source, 'target': target}
if read_only:
mount['read_only'] = True
if create_host_path is not None:
mount['bind'] = {'create_host_path': create_host_path}
mounts.append(mount)
projection = {
'name': 'truf-docker',
'volumes': {
'data': {'name': 'truf-remote-server-data', 'external': True},
},
'services': {
'runtime': {
'network_mode': 'host', 'ports': None, 'cpus': 0.9,
'mem_limit': str(720 * 1024 ** 2), 'volumes': mounts,
},
'edge': {
'network_mode': 'service:runtime', 'cap_add': None,
'image': 'truf-local:edge',
},
},
}
payload = json.dumps(projection).encode('ascii')
installer._validate_compose_projection(payload, installer.SHARED_HOST_PROFILE)
projection['services']['runtime']['ports'] = [{
'target': 443, 'published': '443', 'protocol': 'tcp', 'mode': 'host',
}]
with self.assertRaises(installer.InstallError):
installer._validate_compose_projection(payload.replace(b'"ports": null', b'"ports": []'), installer.SHARED_HOST_PROFILE)
drifted = json.loads(payload)
drifted['services']['runtime']['cpus'] = 2.0
with self.assertRaises(installer.InstallError):
installer._validate_compose_projection(
json.dumps(drifted).encode('ascii'), installer.SHARED_HOST_PROFILE,
)
mounts[1]['source'] = '/etc/truf/runtime'
mounts[1]['bind']['create_host_path'] = True
with self.assertRaises(installer.InstallError):
installer._validate_compose_projection(json.dumps(projection).encode('ascii'))
@unittest.skipIf(os.name == 'nt', 'POSIX durable file operations required')
def test_write_does_not_replace_existing_active_file(self):
with tempfile.TemporaryDirectory() as temporary:
parent = Path(temporary)
target = parent / 'config.yaml'
target.write_bytes(b'original')
os.chmod(target, 0o600)
with mock.patch.object(installer.os, 'fchown'):
installer._write(
target, b'candidate', uid=os.getuid(), gid=os.getgid(),
mode=0o600, replace=False,
)
self.assertEqual(target.read_bytes(), b'original')
self.assertFalse((parent / '.config.yaml.truf-install').exists())
with mock.patch.object(installer.os, 'fchown'):
installer._write(
target, b'replacement', uid=os.getuid(), gid=os.getgid(),
mode=0o600, replace=True,
)
self.assertEqual(target.read_bytes(), b'replacement')
self.assertEqual(stat.S_IMODE(target.stat().st_mode), 0o600)
if __name__ == '__main__':
unittest.main()