feat: Phase 3 — ServiceComposer deps + store install via per-service compose
Unit Tests / test (push) Successful in 11m21s
Unit Tests / test (push) Successful in 11m21s
ServiceStoreManager.install() now delegates container lifecycle to ServiceComposer (per-service docker-compose.yml) instead of appending to a shared compose override. This eliminates IP pool allocation, compose override rendering, and the single-stack docker exec approach. Changes: - service_composer.py: add _resolve_requires(), _resolve_dependents(), reapply_active_services() — dependency graph and startup reapply - service_store_manager.py: rewrite install() and remove() to use ServiceComposer; add _fetch_template(); delete _allocate_service_ip(), _render_compose_override(), _write_compose_override(); remove() now guards against removing services that others depend on - managers.py: pass service_composer= to ServiceStoreManager - Tests: 13 new composer dep tests; TestInstall/TestRemove rewritten for the new composer-driven path; test_optional_services_feature.py updated Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -277,6 +277,39 @@ class ServiceComposer:
|
||||
pass
|
||||
return result
|
||||
|
||||
# ── Dependency resolution ─────────────────────────────────────────────
|
||||
|
||||
def _resolve_requires(self, manifest: Dict, installed_services: Dict) -> Optional[str]:
|
||||
"""Return an error string if any required services are missing, else None."""
|
||||
requires = manifest.get('requires') or []
|
||||
missing = [r for r in requires if r not in installed_services]
|
||||
if missing:
|
||||
return f"Required services not installed: {', '.join(sorted(missing))}"
|
||||
return None
|
||||
|
||||
def _resolve_dependents(self, service_id: str, installed_services: Dict) -> List[str]:
|
||||
"""Return list of installed service IDs that declare service_id in their requires."""
|
||||
dependents = []
|
||||
for svc_id, record in installed_services.items():
|
||||
if svc_id == service_id:
|
||||
continue
|
||||
m = (record.get('manifest') or {})
|
||||
if service_id in (m.get('requires') or []):
|
||||
dependents.append(svc_id)
|
||||
return dependents
|
||||
|
||||
def reapply_active_services(self) -> None:
|
||||
"""Call up() for every installed service that has a compose file. Called at startup."""
|
||||
installed = self.cm.get_installed_services()
|
||||
for svc_id in installed:
|
||||
if not self.has_compose_file(svc_id):
|
||||
logger.warning('reapply_active_services: no compose file for %s, skipping', svc_id)
|
||||
continue
|
||||
result = self.up(svc_id)
|
||||
if not result.get('ok'):
|
||||
logger.warning('reapply_active_services: up failed for %s: %s',
|
||||
svc_id, result.get('error') or result.get('stderr', ''))
|
||||
|
||||
# ── Builtin-service lifecycle (main compose stack) ─────────────────────
|
||||
|
||||
@staticmethod
|
||||
|
||||
Reference in New Issue
Block a user