feat: replace hardcoded service names with ServiceRegistry-driven Caddy and CoreDNS config
Unit Tests / test (push) Failing after 11s
Unit Tests / test (push) Failing after 11s
Previously, CaddyManager and NetworkManager contained hardcoded lists of service names (calendar, files, mail, webdav, etc.), meaning every new service required a code change to appear in Caddy routes and DNS records. Now both managers accept a service_registry parameter and derive their service lists dynamically from the registry at runtime. - CaddyManager: new _build_registry_service_routes() and _http01_service_pairs() methods pull routes from the registry - NetworkManager: new _get_service_subdomains() method returns registry subdomains with a hardcoded fallback when no registry is wired in; _build_dns_records, stale-record detection, and service name sets all use the registry - managers.py: service_registry constructed before network_manager so it can be injected into both CaddyManager and NetworkManager - service_registry.py: validation chokepoint in get_caddy_routes() rejects invalid subdomain/backend values and reserved service names - service_store_manager.py: _validate_manifest now validates top-level subdomain, backend, extra_subdomains, and extra_backends fields - tests: 24 new tests covering registry-driven routing and DNS subdomain generation (test_caddy_registry_integration.py) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -51,6 +51,8 @@ RESERVED_SUBDOMAINS = frozenset([
|
||||
'git', 'registry', 'install',
|
||||
])
|
||||
ENV_VALUE_RE = re.compile(r'^[A-Za-z0-9._@:/+\-= ]*$')
|
||||
SUBDOMAIN_RE = re.compile(r'^[a-z][a-z0-9-]{0,30}$')
|
||||
BACKEND_RE = re.compile(r'^[A-Za-z0-9._-]+:\d{1,5}$')
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -141,19 +143,55 @@ class ServiceStoreManager(BaseServiceManager):
|
||||
f'iptables_rules[].proto must be tcp or udp, got: {proto}'
|
||||
)
|
||||
|
||||
# Caddy route subdomain
|
||||
# Legacy caddy_route dict subdomain (for store manifests using the old format)
|
||||
caddy_route = m.get('caddy_route') or {}
|
||||
if isinstance(caddy_route, dict):
|
||||
subdomain = caddy_route.get('subdomain', '')
|
||||
legacy_sub = caddy_route.get('subdomain', '')
|
||||
else:
|
||||
subdomain = ''
|
||||
if subdomain:
|
||||
if subdomain in RESERVED_SUBDOMAINS:
|
||||
errors.append(f'caddy_route.subdomain is reserved: {subdomain}')
|
||||
elif not re.match(r'^[a-z][a-z0-9-]{0,30}$', subdomain):
|
||||
legacy_sub = ''
|
||||
if legacy_sub:
|
||||
if legacy_sub in RESERVED_SUBDOMAINS:
|
||||
errors.append(f'caddy_route.subdomain is reserved: {legacy_sub}')
|
||||
elif not SUBDOMAIN_RE.match(legacy_sub):
|
||||
errors.append(
|
||||
f'caddy_route.subdomain must match ^[a-z][a-z0-9-]{{0,30}}$, '
|
||||
f'got: {subdomain}'
|
||||
f'got: {legacy_sub}'
|
||||
)
|
||||
|
||||
# Top-level subdomain + backend (consumed by ServiceRegistry.get_caddy_routes)
|
||||
subdomain = m.get('subdomain', '')
|
||||
if subdomain:
|
||||
if subdomain in RESERVED_SUBDOMAINS:
|
||||
errors.append(f'subdomain is reserved: {subdomain}')
|
||||
elif not SUBDOMAIN_RE.match(subdomain):
|
||||
errors.append(
|
||||
f'subdomain must match ^[a-z][a-z0-9-]{{0,30}}$, got: {subdomain}'
|
||||
)
|
||||
|
||||
backend = m.get('backend', '')
|
||||
if backend and not BACKEND_RE.match(backend):
|
||||
errors.append(f'backend must be host:port (e.g. cell-foo:8080), got: {backend}')
|
||||
|
||||
for sub in m.get('extra_subdomains') or []:
|
||||
if not isinstance(sub, str):
|
||||
errors.append('extra_subdomains entries must be strings')
|
||||
elif sub in RESERVED_SUBDOMAINS:
|
||||
errors.append(f'extra_subdomains entry is reserved: {sub}')
|
||||
elif not SUBDOMAIN_RE.match(sub):
|
||||
errors.append(
|
||||
f'extra_subdomains entry must match ^[a-z][a-z0-9-]{{0,30}}$, got: {sub}'
|
||||
)
|
||||
|
||||
for sub, bknd in (m.get('extra_backends') or {}).items():
|
||||
if not isinstance(sub, str) or not SUBDOMAIN_RE.match(sub):
|
||||
errors.append(
|
||||
f'extra_backends key must match ^[a-z][a-z0-9-]{{0,30}}$, got: {sub!r}'
|
||||
)
|
||||
elif sub in RESERVED_SUBDOMAINS:
|
||||
errors.append(f'extra_backends key is reserved: {sub}')
|
||||
if not isinstance(bknd, str) or not BACKEND_RE.match(bknd):
|
||||
errors.append(
|
||||
f'extra_backends[{sub!r}] value must be host:port, got: {bknd!r}'
|
||||
)
|
||||
|
||||
# Env value safety
|
||||
|
||||
Reference in New Issue
Block a user