feat: Phase 2 — remove builtins layer, ServiceRegistry is installed-only
Unit Tests / test (push) Successful in 11m31s

Builtins (email/calendar/files) are no longer baked into the API image.
ServiceRegistry now only knows about installed store services. When nothing
is installed, Caddy and DNS get no service routes — no hardcoded fallback.

Changes:
- service_registry.py: remove _BUILTINS_DIR, _builtin_ids, _builtin_manifest,
  _load_manifest; get() and list_all() now delegate entirely to installed services
- caddy_manager.py: remove _build_core_service_routes(); remove hardcoded
  fallback pairs from _http01_service_pairs(); empty registry → api block only
- network_manager.py: _get_service_subdomains() returns [] when no registry
- api/services/builtins/: deleted (email, calendar, files manifests)
- Tests updated throughout: removed builtin-dependent assertions, added
  installed-service fixtures, updated fallback expectations to api-only

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-29 08:53:44 -04:00
parent 18b50d08c1
commit 0bfe95320b
12 changed files with 419 additions and 766 deletions
+23 -32
View File
@@ -70,20 +70,16 @@ class TestGenerateCaddyfilePicNgo(unittest.TestCase):
# ACME staging hook
self.assertIn('acme_ca {$ACME_CA_URL}', out)
def test_pic_ngo_has_subdomain_service_routes(self):
def test_pic_ngo_has_api_route_without_registry(self):
mgr = _mgr()
identity = {'cell_name': 'alpha', 'domain_mode': 'pic_ngo'}
out = mgr.generate_caddyfile(identity, [])
# Core services get named-matcher subdomain routing
self.assertIn('@calendar host calendar.alpha.pic.ngo', out)
self.assertIn('reverse_proxy cell-radicale:5232', out)
self.assertIn('@mail host mail.alpha.pic.ngo webmail.alpha.pic.ngo', out)
self.assertIn('reverse_proxy cell-rainloop:8888', out)
self.assertIn('@files host files.alpha.pic.ngo', out)
self.assertIn('reverse_proxy cell-filegator:8080', out)
self.assertIn('@webdav host webdav.alpha.pic.ngo', out)
self.assertIn('reverse_proxy cell-webdav:80', out)
# Without a registry only the api block is present
self.assertIn('@api host api.alpha.pic.ngo', out)
self.assertIn('reverse_proxy cell-api:3000', out)
self.assertNotIn('@calendar', out)
self.assertNotIn('@mail', out)
self.assertNotIn('@files', out)
class TestGenerateCaddyfileCloudflare(unittest.TestCase):
@@ -116,9 +112,10 @@ class TestGenerateCaddyfileCloudflare(unittest.TestCase):
self.assertNotIn('*.home.local', out)
# 'custom_domain' must not appear literally as a key in the output
self.assertNotIn('custom_domain', out)
# Service subdomains use the correct public domain
self.assertIn('@calendar host calendar.home.example.com', out)
self.assertIn('@files host files.home.example.com', out)
# Without a registry only the api block is emitted for subdomain routing
self.assertIn('@api host api.home.example.com', out)
self.assertNotIn('@calendar', out)
self.assertNotIn('@files', out)
class TestGenerateCaddyfileDuckDns(unittest.TestCase):
@@ -128,8 +125,9 @@ class TestGenerateCaddyfileDuckDns(unittest.TestCase):
out = mgr.generate_caddyfile(identity, [])
self.assertIn('dns duckdns {$DUCKDNS_TOKEN}', out)
self.assertIn('*.gamma.duckdns.org', out)
self.assertIn('@calendar host calendar.gamma.duckdns.org', out)
self.assertIn('@files host files.gamma.duckdns.org', out)
self.assertIn('@api host api.gamma.duckdns.org', out)
self.assertNotIn('@calendar', out)
self.assertNotIn('@files', out)
class TestGenerateCaddyfileHttp01(unittest.TestCase):
@@ -150,34 +148,27 @@ class TestGenerateCaddyfileHttp01(unittest.TestCase):
self.assertNotIn('dns ', out)
# No explicit tls block — Caddy uses HTTP-01 by default.
self.assertNotIn('tls {', out)
# Core service blocks are always generated
self.assertIn('calendar.delta.noip.me {', out)
self.assertIn('files.delta.noip.me {', out)
self.assertIn('mail.delta.noip.me {', out)
self.assertIn('webmail.delta.noip.me {', out)
self.assertIn('webdav.delta.noip.me {', out)
# Without a registry only the api block is generated
self.assertIn('api.delta.noip.me {', out)
self.assertIn('reverse_proxy cell-radicale:5232', out)
self.assertIn('reverse_proxy cell-filegator:8080', out)
# Installed plugin service block
self.assertNotIn('calendar.delta.noip.me {', out)
self.assertNotIn('files.delta.noip.me {', out)
self.assertNotIn('mail.delta.noip.me {', out)
# Installed plugin service block still works
self.assertIn('chat.delta.noip.me {', out)
self.assertIn('reverse_proxy cell-chat:8090', out)
def test_http01_installed_service_with_core_name_is_skipped(self):
"""An installed service named 'calendar' must not produce a duplicate block."""
def test_http01_installed_service_with_caddy_route_appears(self):
"""An installed service with a caddy_route produces its own per-host block."""
mgr = _mgr()
identity = {
'cell_name': 'delta',
'domain_mode': 'http01',
'domain_name': 'delta.noip.me',
}
services = [{'name': 'calendar', 'caddy_route': 'reverse_proxy cell-other:9000'}]
services = [{'name': 'notes', 'caddy_route': 'reverse_proxy cell-other:9000'}]
out = mgr.generate_caddyfile(identity, services)
# Only one calendar block (the core one)
self.assertEqual(out.count('calendar.delta.noip.me {'), 1)
# The core backend wins
self.assertIn('reverse_proxy cell-radicale:5232', out)
self.assertNotIn('cell-other:9000', out)
self.assertIn('notes.delta.noip.me {', out)
self.assertIn('reverse_proxy cell-other:9000', out)
class TestServiceRoutesIncluded(unittest.TestCase):