from __future__ import annotations from pathlib import Path from app.discovery import DiscoveryManager from app.discovery_store import DiscoveryStore from app.docker_discovery import _kind_and_endpoint, _preflight_candidate from app.models import ( DiscoveryCategory, DiscoveryConfidence, DiscoveryRunStatus, DockerCandidate, DockerPort, DockerSuggestion, HealthStatus, MonitoringState, ProbeEndpointCandidate, ProbeEndpointSource, ProbeReachability, ProbeResult, ServiceCreate, ServiceKind, ) from app.store import Store def _manager(tmp_path: Path) -> DiscoveryManager: store = Store(str(tmp_path / "servicemap.db")) return DiscoveryManager(store, DiscoveryStore(store)) def _candidate( name: str, reference: str, *, project: str | None = None, compose_service: str | None = None, labels: dict[str, str] | None = None, networks: list[str] | None = None, ) -> DockerCandidate: return DockerCandidate( container_id=reference[-12:], name=name, image="example/service:1", state="running", networks=networks or [], ports=[], labels=labels or {}, stable_source_ref=reference, compose_project=project, compose_service=compose_service, category=DiscoveryCategory.APPLICATION, ) def test_internal_docker_dns_is_evidence_not_an_active_probe() -> None: kind, url, host, port, evidence = _kind_and_endpoint( "postgres", [DockerPort(container_port="5432/tcp")], {}, ) assert kind == ServiceKind.TCP assert url is None assert host == "postgres" assert port == 5432 assert "internal container endpoint candidate" in evidence def test_published_port_requires_an_explicit_probe_host() -> None: ports = [DockerPort(container_port="8080/tcp", host_ip="0.0.0.0", host_port=1226)] labels = {"servicemap.probe": "http"} _, unsafe_url, unsafe_host, _, _ = _kind_and_endpoint("app", ports, labels) kind, url, host, port, evidence = _kind_and_endpoint("app", ports, labels, "monitoring-host") assert unsafe_url is None assert unsafe_host == "app" assert kind == ServiceKind.HTTP assert url == "http://monitoring-host:1226" assert host is None and port is None assert "host-published endpoint" in evidence def test_running_container_without_reachable_endpoint_is_runtime_observed(tmp_path: Path) -> None: manager = _manager(tmp_path) candidate = _candidate("worker", "docker:name:worker") candidate.suggestions = [DockerSuggestion(name="worker", kind=ServiceKind.TCP, host="worker", port=9000)] candidate.probe_candidates = [ ProbeEndpointCandidate( source=ProbeEndpointSource.CONTAINER_INTERNAL, kind=ServiceKind.TCP, host="worker", port=9000, reachability=ProbeReachability.INTERNAL_ONLY, ) ] payload = manager._payload_for_candidate(candidate) assert payload.monitoring_enabled is False assert payload.monitoring_state.value == "observed" assert payload.probe_reachability == ProbeReachability.INTERNAL_ONLY def test_docker_healthcheck_observation_does_not_enable_network_probe(tmp_path: Path) -> None: manager = _manager(tmp_path) candidate = _candidate("database", "docker:name:database") candidate.healthcheck_present = True candidate.probe_candidates = [ ProbeEndpointCandidate( source=ProbeEndpointSource.DOCKER_HEALTHCHECK, kind=ServiceKind.TCP, reachability=ProbeReachability.VERIFIED, confidence=DiscoveryConfidence.HIGH, ) ] payload = manager._payload_for_candidate(candidate) assert payload.monitoring_enabled is False assert payload.monitoring_state.value == "docker_health" assert payload.probe_source == ProbeEndpointSource.DOCKER_HEALTHCHECK def test_appops_application_health_is_provenance_not_component_health(tmp_path: Path) -> None: manager = _manager(tmp_path) candidate = _candidate("application", "docker:name:application") candidate.healthcheck_present = True candidate.appops_health = "healthy" candidate.appops_app_id = "northstar" candidate.appops_discovery_confidence = 0.9 payload = manager._payload_for_candidate(candidate) assert payload.monitoring_enabled is False assert payload.monitoring_state == MonitoringState.DOCKER_HEALTH assert payload.probe_source == ProbeEndpointSource.DOCKER_HEALTHCHECK assert payload.runtime_health is None service = manager.store.create_service(payload) manager._record_candidate_evidence(candidate, service) resolution = manager.store.health_resolution(service.id) assert resolution.selected_evidence_id is None assert resolution.evidence[0].scope.value == "application" assert "not used as component" in resolution.explanation def test_compose_reference_prefers_same_project_over_duplicate_stack_name(tmp_path: Path) -> None: manager = _manager(tmp_path) run = manager.discovery_store.start_run() source = _candidate( "api-a", "docker:compose:stack-a:api", project="stack-a", compose_service="api", labels={"com.docker.compose.depends_on": "postgres:service_healthy:false"}, ) postgres_a = _candidate("postgres-a", "docker:compose:stack-a:postgres", project="stack-a", compose_service="postgres") postgres_b = _candidate("postgres-b", "docker:compose:stack-b:postgres", project="stack-b", compose_service="postgres") suggestions = manager._relation_suggestions(run, [source, postgres_a, postgres_b], {}) assert len(suggestions) == 1 assert suggestions[0].target_ref == postgres_a.stable_source_ref assert suggestions[0].confidence == DiscoveryConfidence.HIGH def test_ambiguous_global_name_stays_review_only(tmp_path: Path) -> None: manager = _manager(tmp_path) run = manager.discovery_store.start_run() source = _candidate( "api", "docker:name:api", labels={"servicemap.depends_on": "redis"}, ) redis_a = _candidate("redis-a", "docker:compose:a:redis", project="a", compose_service="redis") redis_b = _candidate("redis-b", "docker:compose:b:redis", project="b", compose_service="redis") suggestions = manager._relation_suggestions(run, [source, redis_a, redis_b], {}) assert len(suggestions) == 1 assert suggestions[0].target_ref is None assert suggestions[0].confidence == DiscoveryConfidence.LOW assert "Manual review required" in " ".join(suggestions[0].evidence) def test_reachability_repair_is_dry_run_first_and_preserves_probe_history(tmp_path: Path) -> None: manager = _manager(tmp_path) candidate = _candidate("legacy-db", "docker:name:legacy-db") candidate.suggestions = [DockerSuggestion(name="legacy-db", kind=ServiceKind.TCP, host="legacy-db", port=5432)] candidate.probe_candidates = [ ProbeEndpointCandidate( source=ProbeEndpointSource.CONTAINER_INTERNAL, kind=ServiceKind.TCP, host="legacy-db", port=5432, reachability=ProbeReachability.INTERNAL_ONLY, ) ] legacy = manager.store.create_service( ServiceCreate( name="legacy-db", kind=ServiceKind.TCP, host="legacy-db", port=5432, source_type="docker", source_ref=candidate.stable_source_ref, ) ) manager.store.save_probe( ProbeResult( id="legacy-probe", service_id=legacy.id, status=HealthStatus.DOWN, latency_ms=None, detail="legacy unreachable internal DNS target", checked_at=legacy.created_at, ) ) run = manager.discovery_store.start_run() source = manager.discovery_store.save_source( source_type="docker", source_id=candidate.stable_source_ref or "", source_name=candidate.name, lifecycle=candidate.lifecycle, metadata={"candidate": candidate.model_dump(mode="json")}, ) manager.discovery_store.save_observation( run_id=run.id, source=source, source_name=candidate.name, lifecycle=candidate.lifecycle, change_type="changed", metadata={"candidate": candidate.model_dump(mode="json")}, ) manager.discovery_store.finish_run(run.id, status=DiscoveryRunStatus.COMPLETED, source_counts={}, diff=run.diff) preview = manager.repair_probe_reachability(run_id=run.id, dry_run=True) assert preview.changed == 1 assert manager.store.get_service(legacy.id).monitoring_enabled is True # type: ignore[union-attr] applied = manager.repair_probe_reachability(run_id=run.id, dry_run=False) updated = manager.store.get_service(legacy.id) assert applied.disabled_unsafe == 1 assert updated is not None and updated.monitoring_enabled is False assert updated.monitoring_state.value == "observed" assert len(manager.store.probe_history(legacy.id)) == 1 def test_container_http_port_is_not_misclassified_by_a_custom_host_port() -> None: kind, url, host, port, evidence = _kind_and_endpoint( "adminer", [DockerPort(container_port="8080/tcp", host_ip="0.0.0.0", host_port=8070)], {}, "127.0.0.1", ) assert kind == ServiceKind.HTTP assert url == "http://127.0.0.1:8070" assert host is None and port is None assert "HTTP-like container port" in evidence def test_published_preflight_requires_a_real_tcp_connection(monkeypatch) -> None: candidate = ProbeEndpointCandidate( source=ProbeEndpointSource.PUBLISHED_PORT, kind=ServiceKind.TCP, host="127.0.0.1", port=3306, ) class _Connection: def __enter__(self): return self def __exit__(self, *_: object) -> None: return None monkeypatch.setattr("app.docker_discovery.socket.create_connection", lambda *_args, **_kwargs: _Connection()) verified = _preflight_candidate(candidate) assert verified.primary is True assert verified.reachability == ProbeReachability.VERIFIED assert verified.preflight_detail == "TCP connect succeeded" def test_http_preflight_records_response_without_scanning_paths(monkeypatch) -> None: candidate = ProbeEndpointCandidate( source=ProbeEndpointSource.PUBLISHED_PORT, kind=ServiceKind.HTTP, url="http://127.0.0.1:8070", ) class _Connection: def __enter__(self): return self def __exit__(self, *_: object) -> None: return None class _Response: status_code = 204 class _Client: def __enter__(self): return self def __exit__(self, *_: object) -> None: return None def request(self, method: str, url: str) -> _Response: assert method == "HEAD" assert url == candidate.url return _Response() monkeypatch.setattr("app.docker_discovery.socket.create_connection", lambda *_args, **_kwargs: _Connection()) monkeypatch.setattr("app.docker_discovery.httpx.Client", lambda **_kwargs: _Client()) verified = _preflight_candidate(candidate) assert verified.primary is True assert verified.preflight_detail == "HTTP 204 responded"