Implement connectivity store services (wireguard-ext, openvpn-client, tor)
Unit Tests / test (push) Successful in 11m31s

- ConnectivityManager: move config dirs to data_dir/services/<id>/config so
  Docker can bind-mount them into store-service containers (Docker resolves
  bind-mount paths on the host, not inside the API container).  Add
  _migrate_legacy_configs to copy existing files from the old config_dir
  location on first boot.

- manifest_validator: add allow_host_network parameter to
  validate_rendered_compose.  When True, waives the external-network
  requirement, permits network_mode: host, and allows devices: — all needed
  by VPN/Tor containers that must share the host network namespace to create
  tun/wg interfaces.  Non-host services are unaffected.

- service_composer: read requires_host_network from the manifest and pass
  allow_host_network=True to validate_rendered_compose for connectivity
  services.

- Tests: update file-path assertions to new data_dir layout; add
  TestMigrateLegacyConfigs, TestValidateRenderedComposeHostNetwork, and
  two TestWriteCompose cases for the host-network path.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-30 10:06:48 -04:00
parent 60601eb4af
commit 7d5c5421f1
6 changed files with 306 additions and 29 deletions
+121
View File
@@ -967,6 +967,127 @@ class TestValidateRenderedCompose(unittest.TestCase):
self.assertTrue(any('bad' in e for e in errs))
# ---------------------------------------------------------------------------
# TestValidateRenderedComposeHostNetwork
# ---------------------------------------------------------------------------
class TestValidateRenderedComposeHostNetwork(unittest.TestCase):
"""Tests for allow_host_network=True — connectivity services."""
_HOST_NET_COMPOSE = (
'services:\n'
' wireguard-ext:\n'
' image: git.pic.ngo/roof/svc-wireguard-ext:latest\n'
' container_name: cell-wg-ext\n'
' restart: unless-stopped\n'
' network_mode: host\n'
' cap_add:\n'
' - NET_ADMIN\n'
' volumes:\n'
' - /app/data/services/wireguard-ext/config:/etc/wireguard\n'
)
def test_host_network_compose_passes_with_flag(self):
ok, errs = validate_rendered_compose(
self._HOST_NET_COMPOSE,
allowed_data_dir='/app/data',
allow_host_network=True,
)
self.assertTrue(ok, errs)
def test_host_network_compose_fails_without_flag(self):
ok, errs = validate_rendered_compose(
self._HOST_NET_COMPOSE,
allowed_data_dir='/app/data',
allow_host_network=False,
)
self.assertFalse(ok)
def test_network_mode_host_rejected_without_flag(self):
yaml_text = (
'services:\n'
' svc:\n'
' image: git.pic.ngo/roof/foo:latest\n'
' network_mode: host\n'
'networks:\n'
' cell-network:\n'
' external: true\n'
)
ok, errs = validate_rendered_compose(yaml_text)
self.assertFalse(ok)
self.assertTrue(any('network_mode' in e for e in errs))
def test_devices_allowed_with_flag(self):
yaml_text = (
'services:\n'
' openvpn-client:\n'
' image: git.pic.ngo/roof/svc-openvpn-client:latest\n'
' container_name: cell-openvpn\n'
' network_mode: host\n'
' cap_add:\n'
' - NET_ADMIN\n'
' devices:\n'
' - /dev/net/tun\n'
' volumes:\n'
' - /app/data/services/openvpn-client/config:/etc/openvpn\n'
)
ok, errs = validate_rendered_compose(
yaml_text,
allowed_data_dir='/app/data',
allow_host_network=True,
)
self.assertTrue(ok, errs)
def test_devices_rejected_without_flag(self):
yaml_text = (
'services:\n'
' svc:\n'
' image: git.pic.ngo/roof/foo:latest\n'
' devices:\n'
' - /dev/net/tun\n'
'networks:\n'
' cell-network:\n'
' external: true\n'
)
ok, errs = validate_rendered_compose(yaml_text)
self.assertFalse(ok)
self.assertTrue(any('devices' in e for e in errs))
def test_no_external_network_ok_with_flag(self):
yaml_text = (
'services:\n'
' tor:\n'
' image: git.pic.ngo/roof/svc-tor:latest\n'
' network_mode: host\n'
)
ok, errs = validate_rendered_compose(yaml_text, allow_host_network=True)
self.assertTrue(ok, errs)
def test_privileged_still_rejected_with_flag(self):
yaml_text = (
'services:\n'
' svc:\n'
' image: git.pic.ngo/roof/foo:latest\n'
' network_mode: host\n'
' privileged: true\n'
)
ok, errs = validate_rendered_compose(yaml_text, allow_host_network=True)
self.assertFalse(ok)
self.assertTrue(any('privileged' in e for e in errs))
def test_non_host_network_mode_rejected_with_flag(self):
"""When allow_host_network=True, only 'host' is accepted as network_mode."""
yaml_text = (
'services:\n'
' svc:\n'
' image: git.pic.ngo/roof/foo:latest\n'
' network_mode: none\n'
)
ok, errs = validate_rendered_compose(yaml_text, allow_host_network=True)
self.assertFalse(ok)
self.assertTrue(any('network_mode' in e for e in errs))
# ---------------------------------------------------------------------------
# TestValidateProvisionHook
# ---------------------------------------------------------------------------